feat(rfa-ai): Complete RFA Approval Refactor and AI Model Revision
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
// File: tests/unit/review-team/aggregate-status.service.spec.ts
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AggregateStatusService } from '../../../src/modules/review-team/services/aggregate-status.service';
|
||||
import { ReviewTask } from '../../../src/modules/review-team/entities/review-task.entity';
|
||||
import {
|
||||
ReviewTaskStatus,
|
||||
ConsensusDecision,
|
||||
} from '../../../src/modules/common/enums/review.enums';
|
||||
|
||||
describe('AggregateStatusService', () => {
|
||||
let service: AggregateStatusService;
|
||||
let _taskRepo: Repository<ReviewTask>;
|
||||
|
||||
const mockTaskRepo = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AggregateStatusService,
|
||||
{
|
||||
provide: getRepositoryToken(ReviewTask),
|
||||
useValue: mockTaskRepo,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AggregateStatusService>(AggregateStatusService);
|
||||
_taskRepo = module.get<Repository<ReviewTask>>(
|
||||
getRepositoryToken(ReviewTask)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getForRevision', () => {
|
||||
it('should return 0 status if no tasks exist', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
const result = await service.getForRevision(1);
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.completionPct).toBe(0);
|
||||
expect(result.isAllComplete).toBe(false);
|
||||
});
|
||||
|
||||
it('should calculate counts correctly', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
{ status: ReviewTaskStatus.PENDING },
|
||||
{ status: ReviewTaskStatus.IN_PROGRESS },
|
||||
{ status: ReviewTaskStatus.DELEGATED },
|
||||
{ status: ReviewTaskStatus.EXPIRED },
|
||||
]);
|
||||
|
||||
const result = await service.getForRevision(1);
|
||||
|
||||
expect(result.total).toBe(6);
|
||||
expect(result.completed).toBe(2);
|
||||
expect(result.pending).toBe(1);
|
||||
expect(result.inProgress).toBe(1);
|
||||
expect(result.delegated).toBe(1);
|
||||
expect(result.expired).toBe(1);
|
||||
expect(result.completionPct).toBe(33);
|
||||
expect(result.isAllComplete).toBe(false);
|
||||
expect(result.hasExpired).toBe(true);
|
||||
});
|
||||
|
||||
it('should return isAllComplete true if all tasks are COMPLETED', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
]);
|
||||
|
||||
const result = await service.getForRevision(1);
|
||||
expect(result.isAllComplete).toBe(true);
|
||||
expect(result.completionPct).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isReadyForConsensus', () => {
|
||||
it('should return true if all complete', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
]);
|
||||
expect(await service.isReadyForConsensus(1)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if not all complete', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.PENDING },
|
||||
]);
|
||||
expect(await service.isReadyForConsensus(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateConsensus', () => {
|
||||
it('should return PENDING if no completed tasks', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.PENDING
|
||||
);
|
||||
});
|
||||
|
||||
it('should return REJECTED if any Code 3 exists', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '3' } },
|
||||
]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.REJECTED
|
||||
);
|
||||
});
|
||||
|
||||
it('should return APPROVED if all are 1A or 1B', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '1B' } },
|
||||
]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.APPROVED
|
||||
);
|
||||
});
|
||||
|
||||
it('should return APPROVED_WITH_COMMENTS if any Code 2 exists and no Code 3', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '2' } },
|
||||
]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.APPROVED_WITH_COMMENTS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMostRestrictiveResponseCode', () => {
|
||||
it('should return 1A if no completed tasks', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('1A');
|
||||
});
|
||||
|
||||
it('should return 3 if any Code 3 exists', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '2' } },
|
||||
{ responseCode: { code: '3' } },
|
||||
]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('3');
|
||||
});
|
||||
|
||||
it('should return 2 if Code 2 exists and no Code 3', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '1B' } },
|
||||
{ responseCode: { code: '2' } },
|
||||
]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('2');
|
||||
});
|
||||
|
||||
it('should return 1B if Code 1B exists and no Code 2 or 3', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '1B' } },
|
||||
]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('1B');
|
||||
});
|
||||
|
||||
it('should return 1A if only Code 1A exists', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([{ responseCode: { code: '1A' } }]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('1A');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -82,6 +82,7 @@ describe('TaskCreationService delegation resolution', () => {
|
||||
|
||||
const tasks = await service.createParallelTasks(
|
||||
100,
|
||||
'rfa-public-id',
|
||||
team.publicId,
|
||||
new Date('2026-05-20T00:00:00.000Z'),
|
||||
manager as unknown as EntityManager
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// File: tests/unit/review-team/veto-override.service.spec.ts
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import {
|
||||
VetoOverrideService,
|
||||
VetoOverrideDto,
|
||||
} from '../../../src/modules/review-team/services/veto-override.service';
|
||||
import { ReviewTask } from '../../../src/modules/review-team/entities/review-task.entity';
|
||||
import { ApprovalListenerService } from '../../../src/modules/distribution/services/approval-listener.service';
|
||||
import { ConsensusDecision } from '../../../src/modules/common/enums/review.enums';
|
||||
import { NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
|
||||
describe('VetoOverrideService', () => {
|
||||
let service: VetoOverrideService;
|
||||
let _taskRepo: Repository<ReviewTask>;
|
||||
let approvalListenerService: ApprovalListenerService;
|
||||
|
||||
const mockTaskRepo = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockApprovalListenerService = {
|
||||
onConsensusReached: jest.fn(),
|
||||
};
|
||||
|
||||
const mockDataSource = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
VetoOverrideService,
|
||||
{
|
||||
provide: getRepositoryToken(ReviewTask),
|
||||
useValue: mockTaskRepo,
|
||||
},
|
||||
{
|
||||
provide: ApprovalListenerService,
|
||||
useValue: mockApprovalListenerService,
|
||||
},
|
||||
{
|
||||
provide: DataSource,
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<VetoOverrideService>(VetoOverrideService);
|
||||
_taskRepo = module.get<Repository<ReviewTask>>(
|
||||
getRepositoryToken(ReviewTask)
|
||||
);
|
||||
approvalListenerService = module.get<ApprovalListenerService>(
|
||||
ApprovalListenerService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('executeOverride', () => {
|
||||
const validDto: VetoOverrideDto = {
|
||||
rfaRevisionId: 1,
|
||||
rfaPublicId: 'rfa-uuid',
|
||||
rfaRevisionPublicId: 'rev-uuid',
|
||||
projectId: 10,
|
||||
documentTypeCode: 'SD',
|
||||
overrideReason: 'This is a valid justification for override.',
|
||||
overriddenByUserId: 1,
|
||||
};
|
||||
|
||||
it('should throw NotFoundException if no tasks found', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
await expect(service.executeOverride(validDto)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if no Code 3 veto found', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ id: 1, responseCode: { code: '1A' } },
|
||||
{ id: 2, responseCode: { code: '2' } },
|
||||
]);
|
||||
await expect(service.executeOverride(validDto)).rejects.toThrow(
|
||||
ForbiddenException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if reason is too short', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ id: 1, responseCode: { code: '3' } },
|
||||
]);
|
||||
const shortDto = { ...validDto, overrideReason: 'Too short' };
|
||||
await expect(service.executeOverride(shortDto)).rejects.toThrow(
|
||||
ForbiddenException
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully execute override and call approval listener', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ id: 1, responseCode: { code: '3' } },
|
||||
]);
|
||||
|
||||
const result = await service.executeOverride(validDto);
|
||||
|
||||
expect(result.decision).toBe(ConsensusDecision.OVERRIDDEN);
|
||||
expect(approvalListenerService.onConsensusReached).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rfaPublicId: validDto.rfaPublicId,
|
||||
decision: ConsensusDecision.OVERRIDDEN,
|
||||
responseCode: '1A',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user