Assessor_2.0/tests/domain/regulatory-mapping.test.ts

180 lines
9.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
groundRegulatoryCandidateAssessmentResult,
groundRegulatoryMappingResult,
regulatoryCandidateAssessmentValidationErrors,
regulatoryMappingResultValidationErrors,
regulatoryMappingRunValidationErrors,
selectRegulatoryMappingsFromAssessments,
type RegulatoryCandidateAssessmentResult,
type RegulatoryMappingRun
} from '../../src/domain/regulatory-mapping';
const grounded = new Map([['5.1', new Set(['semantic-5.1', 'reference-5.1'])]]);
describe('regulatory mapping output validation', () => {
it('requires one grounded assessment for every supplied candidate', () => {
const allowed = new Map([
['5.1', new Set(['semantic-5.1'])],
['8.2', new Set(['semantic-8.2'])]
]);
const valid = {
schemaVersion: 1, outcome: 'assessed', assessments: [
{ controlId: '5.1', relevance: 'primary', confidence: 'high', rationale: 'Direct.', evidenceChunkIds: ['semantic-5.1'] },
{ controlId: '8.2', relevance: 'irrelevant', confidence: 'high', rationale: 'Only broad overlap.', evidenceChunkIds: ['semantic-8.2'] }
]
};
expect(regulatoryCandidateAssessmentValidationErrors(valid, allowed)).toEqual([]);
expect(regulatoryCandidateAssessmentValidationErrors({ ...valid, assessments: valid.assessments.slice(0, 1) }, allowed))
.toEqual(expect.arrayContaining([expect.stringContaining('candidate control 8.2'), expect.stringContaining('every supplied candidate')]));
expect(regulatoryCandidateAssessmentValidationErrors({ ...valid, assessments: [...valid.assessments, valid.assessments[0]] }, allowed))
.toEqual(expect.arrayContaining([expect.stringContaining('duplicates control 5.1')]));
});
it('grounds assessments and deterministically selects at most one primary plus three supporting controls', () => {
const allowed = new Map([
['5.1', new Set(['chunk-5.1'])], ['5.2', new Set(['chunk-5.2'])], ['5.3', new Set(['chunk-5.3'])],
['5.4', new Set(['chunk-5.4'])], ['5.5', new Set(['chunk-5.5'])]
]);
const assessment = (controlId: string, relevance: 'primary' | 'supporting' | 'irrelevant', confidence: 'low' | 'medium' | 'high') => ({
controlId, relevance, confidence, rationale: `Synthetic ${controlId}.`, evidenceChunkIds: [`chunk-${controlId}`, 'foreign']
});
const groundedAssessment = groundRegulatoryCandidateAssessmentResult({
schemaVersion: 1, outcome: 'assessed', assessments: [
assessment('5.1', 'primary', 'medium'), assessment('5.2', 'primary', 'high'),
assessment('5.3', 'supporting', 'high'), assessment('5.4', 'supporting', 'medium'), assessment('5.5', 'supporting', 'low')
]
}, allowed) as RegulatoryCandidateAssessmentResult;
expect(regulatoryCandidateAssessmentValidationErrors(groundedAssessment, allowed)).toEqual([]);
const selected = selectRegulatoryMappingsFromAssessments(groundedAssessment, ['5.1', '5.2', '5.3', '5.4', '5.5']);
expect(selected.mappings.map(item => `${item.controlId}:${item.relationship}`)).toEqual([
'5.2:primary', '5.1:supporting', '5.3:supporting', '5.4:supporting'
]);
expect(selected.mappings.every(item => item.evidenceChunkIds.length === 1)).toBe(true);
});
it('derives supporting-only and no-relevant-control outcomes without forcing a primary', () => {
const supporting = selectRegulatoryMappingsFromAssessments({
schemaVersion: 1, outcome: 'assessed', assessments: [{
controlId: '5.1', relevance: 'supporting', confidence: 'medium', rationale: 'Material support.', evidenceChunkIds: ['semantic-5.1']
}]
}, ['5.1']);
expect(supporting).toMatchObject({ outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'supporting' }] });
const irrelevant = selectRegulatoryMappingsFromAssessments({
schemaVersion: 1, outcome: 'assessed', assessments: [{
controlId: '5.1', relevance: 'irrelevant', confidence: 'high', rationale: 'Only topic overlap.', evidenceChunkIds: ['semantic-5.1']
}]
}, ['5.1']);
expect(irrelevant).toMatchObject({ outcome: 'no_relevant_control', mappings: [] });
});
it('accepts a grounded proposal and rejects unknown controls, duplicate controls, and foreign evidence', () => {
const valid = {
schemaVersion: 1, outcome: 'mapped', mappings: [{
controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.',
evidenceChunkIds: ['semantic-5.1', 'reference-5.1']
}]
};
expect(regulatoryMappingResultValidationErrors(valid, grounded)).toEqual([]);
expect(regulatoryMappingResultValidationErrors({ ...valid, mappings: [
...valid.mappings, { ...valid.mappings[0], evidenceChunkIds: ['other'] }
] }, grounded)).toEqual(expect.arrayContaining([
expect.stringContaining('duplicates control 5.1'),
expect.stringContaining('not grounded')
]));
expect(regulatoryMappingResultValidationErrors({ ...valid, mappings: [{ ...valid.mappings[0], controlId: '9.9' }] }, grounded))
.toEqual(expect.arrayContaining([expect.stringContaining('unknown candidate control')]));
});
it('trims ungrounded evidence ids, drops fully ungrounded known-control mappings, and leaves unknown controls for validation', () => {
const result = groundRegulatoryMappingResult({
schemaVersion: 1, outcome: 'mapped', mappings: [
{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'One good id kept.', evidenceChunkIds: ['semantic-5.1', 'foreign'] },
{ controlId: '5.1', relationship: 'supporting', confidence: 'low', rationale: 'No real evidence.', evidenceChunkIds: ['foreign', 'other'] }
]
}, grounded) as { mappings: Array<{ evidenceChunkIds: string[] }> };
expect(result.mappings).toHaveLength(1);
expect(result.mappings[0].evidenceChunkIds).toEqual(['semantic-5.1']);
const unknown = groundRegulatoryMappingResult({
schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '9.9', relationship: 'primary', confidence: 'high', rationale: 'x', evidenceChunkIds: ['semantic-5.1'] }]
}, grounded) as { mappings: unknown[] };
expect(unknown.mappings).toHaveLength(1);
expect(regulatoryMappingResultValidationErrors(unknown, grounded)).toEqual(expect.arrayContaining([expect.stringContaining('unknown candidate control')]));
});
it('requires refusal outcomes to contain no mappings and a bounded reason', () => {
expect(regulatoryMappingResultValidationErrors({
schemaVersion: 1, outcome: 'no_relevant_control', reason: 'No direct relationship.', mappings: []
}, grounded)).toEqual([]);
expect(regulatoryMappingResultValidationErrors({
schemaVersion: 1, outcome: 'insufficient_context', reason: 'Unknown.', mappings: [{
controlId: '5.1', relationship: 'supporting', confidence: 'low', rationale: 'Synthetic.', evidenceChunkIds: ['semantic-5.1']
}]
}, grounded)).toContain('A refusal outcome must not include mappings.');
});
it('accepts a mapped result with supporting controls and no primary control', () => {
expect(regulatoryMappingResultValidationErrors({
schemaVersion: 1, outcome: 'mapped', mappings: [{
controlId: '5.1', relationship: 'supporting', confidence: 'medium', rationale: 'Relevant, but not uniquely primary.',
evidenceChunkIds: ['semantic-5.1']
}]
}, grounded)).toEqual([]);
});
it('accepts a twelve-control shortlist and rejects a thirteenth control', () => {
const candidate = (index: number) => {
const controlId = `8.${index + 1}`;
return {
controlId,
annexAControlId: `A.${controlId}`,
title: `Synthetic control ${controlId}`,
score: 1 - index / 100,
evidence: [{
chunkId: `chunk-${controlId}`,
textSha256: 'a'.repeat(64),
sourceSha256: 'b'.repeat(64),
sourceStandard: 'ISO/IEC 27002' as const,
sectionKind: 'control' as const,
pageStart: 1,
pageEnd: 1,
text: 'Synthetic evidence.'
}]
};
};
const run: RegulatoryMappingRun = {
version: 1,
id: 'mapping-1',
ruleKey: 'pingcastle:synthetic',
sourceTool: 'pingcastle',
status: 'running',
queryHash: 'c'.repeat(64),
preparedQuery: {
schemaVersion: 2,
query: 'Synthetic full context.',
retrievalQuery: 'Synthetic concise query.',
specificity: 'rule',
publicTopics: []
},
corpusId: 'synthetic',
corpusVersion: '1',
sourceHashes: ['b'.repeat(64)],
embeddingModel: 'synthetic',
retrievalVersion: 'synthetic',
promptVersion: 'synthetic',
outputSchemaVersion: 1,
model: 'synthetic',
createdAt: '2026-09-17T00:00:00.000Z',
updatedAt: '2026-09-17T00:00:00.000Z',
candidates: Array.from({ length: 12 }, (_, index) => candidate(index))
};
expect(regulatoryMappingRunValidationErrors(run)).toEqual([]);
expect(regulatoryMappingRunValidationErrors({ ...run, candidates: [...run.candidates, candidate(12)] }))
.toContain('Regulatory mapping candidates must contain one to twelve controls when processing has started.');
});
});