import { describe, expect, it } from 'vitest'; import { aiOverviewResultValidationErrors, aiOverviewScopeKey, normalizeAiOverviewScope } from '../../src/domain/ai-overview'; import { splitPrivacySafeInput, type PrivacySafeInput } from '../../server/ai/ai-overview-service'; import { validAiOverviewResult } from '../fixtures/ai-overview'; describe('AI overview contract', () => { it('normalizes timeslot identity deterministically', () => { const scope = normalizeAiOverviewScope({ assessmentIds: ['b', 'a', 'a'], scanRunIds: ['2', '1'] }); expect(scope).toEqual({ assessmentIds: ['a', 'b'], scanRunIds: ['1', '2'] }); expect(aiOverviewScopeKey('project', scope)).toBe('project::assessments::a|b::scan-runs::1|2'); }); it('rejects invented source IDs and malformed roadmap phases', () => { const result = validAiOverviewResult(); result.roadmap[2].phase = 4; expect(aiOverviewResultValidationErrors(result, new Set(['OTHER']), new Set(['RULE-GAP']))) .toEqual(expect.arrayContaining([expect.stringContaining('unknown source rule ID'), expect.stringContaining('roadmap[2].phase')])); }); it('rejects direct identifiers returned by the model', () => { const result = validAiOverviewResult(); result.executiveSummary = 'Contact admin@example.test at 192.0.2.10.'; expect(aiOverviewResultValidationErrors(result)).toContain('AI overview output contains an email address or IP address.'); }); it('splits only between complete privacy-safe array entries', () => { const input: PrivacySafeInput = { schemaVersion: 1, summary: { totalResults: 2 }, findings: [ { id: 'RULE-1', title: 'A'.repeat(120), outcome: 'fail' }, { id: 'RULE-2', title: 'B'.repeat(120), outcome: 'fail' } ], notRunChecks: [], environmentFacts: [] }; const chunks = splitPrivacySafeInput(input, 350); expect(chunks).toHaveLength(2); expect(chunks.flatMap(chunk => chunk.findings).map(item => item.id)).toEqual(['RULE-1', 'RULE-2']); expect(chunks.every(chunk => JSON.stringify(chunk).length <= 350)).toBe(true); }); });