Assessor_2.0/tests/server/ai-overview-service.test.ts

77 lines
4.6 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { AiOverviewService } from '../../server/ai/ai-overview-service';
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
import { validAiOverviewResult } from '../fixtures/ai-overview';
const stamp = '2026-09-14T08:00:00.000Z';
function syntheticResult(): UnifiedResultProjection {
return {
resultProjectionId: 'projection-1', sourceKind: 'scanner-finding', sourceResultId: 'occurrence-1',
projectId: 'project-1', assessmentId: 'assessment-1', collectionRunId: 'run-1', scanRunId: 'run-1',
sourceTool: 'Synthetic Scanner', sourceRuleId: 'RULE-1', title: 'Synthetic finding', description: 'Synthetic description',
recommendation: 'Synthetic recommendation', severityOriginal: 'high', severityNormalized: 'high',
outcomeOriginal: 'fail', outcomeNormalized: 'fail', technicalExecutionState: 'completed', resultType: 'compliance_check',
isIoe: false, isDeviation: true, lifecycleState: 'new', mappingConfidence: 1,
reviewStatus: 'unreviewed', importedAt: stamp, createdAt: stamp, categories: []
};
}
describe('AI overview service', () => {
it('uses the complete timeslot query, persists a successful run, and honors idempotency', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject({ id: 'project-1', name: 'Synthetic', description: '', attributes: {}, createdAt: stamp, updatedAt: stamp });
await repository.saveAssessment({ id: 'assessment-1', projectId: 'project-1', name: 'Synthetic timeslot', createdAt: stamp, updatedAt: stamp });
const query = vi.spyOn(repository, 'queryUnifiedResults').mockResolvedValue({ total: 1, items: [syntheticResult()] });
const serviceResult = validAiOverviewResult();
serviceResult.coverageGaps = [];
serviceResult.environmentOverview.factsUsed = [];
const complete = vi.fn(async () => ({ value: serviceResult, usage: { totalTokens: 30 } }));
const service = new AiOverviewService(repository, {
client: { model: 'work-model', complete }, now: () => new Date(stamp)
});
const request = { assessmentIds: ['assessment-1'], scanRunIds: [], idempotencyKey: 'request-1234' };
const started = await service.start('project-1', request);
expect(started.state).toBe('queued');
await service.close();
const completed = await service.get('project-1', request);
expect(completed).toMatchObject({ state: 'succeeded', stale: false, run: { model: 'work-model', callCount: 1, tokenUsage: { totalTokens: 30 } } });
expect(query).toHaveBeenCalledWith({
audience: 'result-db', projectId: 'project-1', assessmentIds: ['assessment-1'], scanRunIds: [], offset: 0, limit: 500
});
expect((await repository.exportProject('project-1')).aiOverviewRuns).toHaveLength(1);
await service.start('project-1', request);
await service.close();
expect(complete).toHaveBeenCalledTimes(1);
expect(await repository.listAiOverviewRuns('project-1')).toHaveLength(1);
});
it('keeps the previous success when a later regeneration fails', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject({ id: 'project-1', name: 'Synthetic', description: '', attributes: {}, createdAt: stamp, updatedAt: stamp });
await repository.saveAssessment({ id: 'assessment-1', projectId: 'project-1', name: 'Synthetic timeslot', createdAt: stamp, updatedAt: stamp });
vi.spyOn(repository, 'queryUnifiedResults').mockResolvedValue({ total: 1, items: [syntheticResult()] });
const serviceResult = validAiOverviewResult();
serviceResult.coverageGaps = [];
serviceResult.environmentOverview.factsUsed = [];
const complete = vi.fn()
.mockResolvedValueOnce({ value: serviceResult })
.mockResolvedValueOnce({ value: { schemaVersion: 1 } });
const service = new AiOverviewService(repository, { client: { model: 'work-model', complete }, now: () => new Date(stamp) });
await service.start('project-1', { assessmentIds: ['assessment-1'], scanRunIds: [], idempotencyKey: 'request-good' });
await service.close();
await service.start('project-1', { assessmentIds: ['assessment-1'], scanRunIds: [], idempotencyKey: 'request-fail' });
await service.close();
const runs = await repository.listAiOverviewRuns('project-1');
expect(runs.map(run => run.status).sort()).toEqual(['failed', 'succeeded']);
expect(runs.find(run => run.status === 'succeeded')?.result?.executiveSummary).toBe('Synthetic executive summary.');
expect(runs.find(run => run.status === 'failed')?.errorCode).toBe('ai_output_invalid');
});
});