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, frameworkReferences: [], 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'); }); });