Assessor_2.0/tests/compliance/ai-overview.test.ts

59 lines
3.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { aiOverviewResultValidationErrors, aiOverviewScopeKey, directIdentifierLiterals, normalizeAiOverviewScope } from '../../src/compliance/ai-overview';
import { splitPrivacySafeInput, type PrivacySafeInput } from '../../server/compliance/ai-overview-service';
import { validAiOverviewResult } from './ai-overview-fixtures';
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, undefined, undefined, undefined, new Set()))
.toContain('AI overview output contains an email address or IP address.');
});
it('accepts address-shaped CIS rule numbers and range boundaries only when the input carries them', () => {
const input = JSON.stringify({ findings: [
{ id: '18.10.42.11', title: '18.10.42.11 (L1) Ensure ...' },
{ id: '18.9.20.1.1', recommendation: 'Allow 0.0.0.0 to 9.255.255.255.' }
] });
const literals = directIdentifierLiterals(input);
expect([...literals].sort()).toEqual(['0.0.0.0', '18.10.42.11', '9.255.255.255']);
const result = validAiOverviewResult();
result.themes[0].findingIds = ['18.10.42.11', '18.9.20.1.1'];
result.themes[0].summary = 'CIS 18.10.42.11 and 18.9.20.1.1 allow 0.0.0.0 to 9.255.255.255.';
const allowed = new Set(['RULE-1', '18.10.42.11', '18.9.20.1.1']);
expect(aiOverviewResultValidationErrors(result, allowed, new Set(['RULE-GAP']), undefined, literals)).toEqual([]);
result.executiveSummary = 'The host 192.0.2.10 is exposed.';
expect(aiOverviewResultValidationErrors(result, allowed, new Set(['RULE-GAP']), undefined, literals))
.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);
});
});