import { describe, expect, it } from 'vitest'; import type { SourceExtractionResult } from '../../src/domain/source-extraction'; import { mapFindingCandidates } from '../../src/importers/candidate-mapping'; import { parseCsvDocument } from '../../src/importers/csv-importer'; import { parseJsonDocument } from '../../src/importers/json-importer'; import { extractSourceDocument } from '../../src/importers/source-extractor-registry'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; import { applyRecognitionOverrides, recognizeImportDocument } from '../../src/recognition/recognition-engine'; import { sourceMetadata } from './import-source-fixtures'; function withExtractor(extraction: SourceExtractionResult, id: string, version: string): SourceExtractionResult { return { ...extraction, extractor: { kind: 'source-extractor', id, version }, processing: { ...extraction.processing, processor: { kind: 'source-extractor', id, version } } }; } describe('finding candidate mapping', () => { it('maps neutral fields with the versioned profile and preserves source provenance', () => { const text = 'Rule ID;Severity;Title\nSYN-1;High;Synthetic finding'; const document = parseCsvDocument(sourceMetadata('synthetic.csv', text.length, 'text/csv'), text); const extraction = extractSourceDocument(document); const mapping = mapFindingCandidates(extraction, genericProfile); expect(mapping).toMatchObject({ version: 1, extractor: extraction.extractor, mappingProfile: { kind: 'mapping-profile', id: 'generic', version: '1.0.0' }, processing: { stage: 'mapping', status: 'complete' } }); expect(mapping.fieldDecisions.map(item => [item.sourceLabel, item.appliedRole])).toEqual([ ['Rule ID', 'rule-id'], ['Severity', 'severity'], ['Title', 'title'] ]); expect(mapping.candidates[0].fields[0]).toMatchObject({ originalValue: 'SYN-1', decodedValue: 'SYN-1', role: 'rule-id', source: { fileName: 'synthetic.csv', tableIndex: 0, rowIndex: 1, columnIndex: 0 } }); }); it('keeps extractor variants independent from a shared mapping profile', () => { const text = 'Title;Severity\nSynthetic finding;Medium'; const document = parseCsvDocument(sourceMetadata('synthetic.csv', text.length, 'text/csv'), text); const extraction = extractSourceDocument(document); const first = mapFindingCandidates(withExtractor(extraction, 'variant-a', '1.0.0'), genericProfile); const second = mapFindingCandidates(withExtractor(extraction, 'variant-b', '2.0.0'), genericProfile); expect(first.extractor).not.toEqual(second.extractor); expect(first.mappingProfile).toEqual(second.mappingProfile); expect(first.fieldDecisions).toEqual(second.fieldDecisions); expect(first.candidates).toEqual(second.candidates); }); it('uses the same field-role evidence as document recognition', () => { const text = 'Rule ID;Severity;Title\nSYN-1;High;Synthetic finding'; const document = parseCsvDocument(sourceMetadata('synthetic.csv', text.length, 'text/csv'), text); const recognition = recognizeImportDocument(document); const mapping = mapFindingCandidates(extractSourceDocument(document), genericProfile); expect(mapping.fieldDecisions.map(item => item.appliedRole)).toEqual(recognition.fields.map(item => item.appliedRole)); expect(mapping.fieldDecisions.map(item => item.confidence)).toEqual(recognition.fields.map(item => item.confidence)); }); it('carries an explicitly confirmed field role into candidate mapping', () => { const text = 'Name;Severity\nSynthetic system;High'; const document = parseCsvDocument(sourceMetadata('synthetic.csv', text.length, 'text/csv'), text); const recognition = recognizeImportDocument(document); const confirmed = applyRecognitionOverrides(recognition, { fieldRoles: [{ tableId: recognition.fields[0].tableId, columnIndex: 0, role: 'asset' }] }); const mapping = mapFindingCandidates(extractSourceDocument(document), genericProfile, confirmed); expect(mapping.fieldDecisions[0]).toMatchObject({ appliedRole: 'asset', decisionSource: 'user-confirmed', requiresConfirmation: false }); expect(mapping.candidates[0].fields[0].role).toBe('asset'); }); it('applies explicit native candidate-field confirmation without changing tabular decisions', () => { const text = JSON.stringify([{ id: 'SYN-1', detail: 'Synthetic detail' }]); const document = parseJsonDocument(sourceMetadata('synthetic.json', text.length, 'application/json'), text); const extraction = extractSourceDocument(document); const initial = mapFindingCandidates(extraction, genericProfile); const confirmed = mapFindingCandidates(extraction, genericProfile, undefined, { candidateFieldRoles: [{ decisionId: initial.fieldDecisions[0].id, role: 'title' }] }); expect(confirmed.fieldDecisions[0]).toMatchObject({ appliedRole: 'title', decisionSource: 'user-confirmed', requiresConfirmation: false }); expect(confirmed.candidates[0].fields[0].role).toBe('title'); }); it('retains nested native JSON originals while adding roles', () => { const text = JSON.stringify([{ id: 'SYN-1', details: { enabled: true, score: 7 } }]); const document = parseJsonDocument(sourceMetadata('synthetic.json', text.length, 'application/json'), text); const mapping = mapFindingCandidates(extractSourceDocument(document), genericProfile); expect(mapping.candidates[0].fields.find(field => field.key === 'details')).toMatchObject({ originalValue: { enabled: true, score: 7 }, source: { objectPath: '$[0].details' } }); }); });