import { describe, expect, it } from 'vitest'; import type { ImportedDataset } from '../../src/domain/dataset'; import { makeSheet } from '../../src/importers/sheet-factory'; import type { TimeslotComparisonReport } from '../../src/comparison/timeslot-comparison-report'; import { withScannerValueChanges } from '../../src/comparison/timeslot-value-comparison'; // Scanner value changes (user decision 2026-10-13): a finding still deviating in A and B whose Finding Reader // Problem values differ is `value-changed` (neutral). Synthetic Purple Knight records only. function timeslot(prefix: string, rows: Array<[string, string, string]>): ImportedDataset { const sheet = makeSheet('Synthetic', [['Name', 'Status', 'Number of results', 'Result message'], ...rows.map(([name, count, message]) => [name, 'IOE Found', count, message])], '', 'synthetic.xlsx', { isProjectSheet: true }); sheet.toolId = 'purple-knight'; sheet.analysis.records.forEach((record, index) => { record.findingOccurrenceId = `${prefix}-${index + 1}`; }); return { fileName: 'Synthetic', sheets: [sheet], scanTool: 'Purple Knight', profileId: 'purple-knight' } as ImportedDataset; } const entry = (index: number) => ({ sourceId: 'purple-knight', change: 'unchanged' as const, side: 'baseline' as const, recordIds: [`a-${index}`], counterpartIds: [`b-${index}`], reasons: [] }); const report = { changes: [entry(1), entry(2)], summary: { resolved: 0, unchanged: 2, 'value-changed': 0, unverified: 0, 'not-comparable': 0, 'not-applicable': 0, new: 0, regressed: 0, targetNotComparable: 0 } } as unknown as TimeslotComparisonReport; describe('Comparison Engine scanner value changes', () => { it('marks a still deviating finding with different Problem values as value changed and keeps an identical one unchanged', () => { const a = timeslot('a', [['Synthetic templates', '1', 'Found 1 synthetic template'], ['Synthetic accounts', '4', 'Found 4 synthetic accounts']]); const b = timeslot('b', [['Synthetic templates', '3', 'Found 3 synthetic templates'], ['Synthetic accounts', '4', 'Found 4 synthetic accounts']]); const result = withScannerValueChanges(report, a, b); expect(result.changes.map(item => item.change)).toEqual(['value-changed', 'unchanged']); expect(result.changes[0].problem).toEqual({ baseline: [{ label: 'Result', value: 'Found 1 synthetic template' }, { label: 'Affected', value: '1' }], target: [{ label: 'Result', value: 'Found 3 synthetic templates' }, { label: 'Affected', value: '3' }] }); expect(result.summary).toMatchObject({ unchanged: 1, 'value-changed': 1 }); // The input report is not changed; only the returned one carries the value change. expect(report.changes[0].change).toBe('unchanged'); }); it('never turns another change into a value change and leaves a report without Work data as it is', () => { const resolved = { ...report, changes: [{ ...entry(1), change: 'resolved' as const }] } as TimeslotComparisonReport; const a = timeslot('a', [['Synthetic templates', '1', 'Found 1']]); const b = timeslot('b', [['Synthetic templates', '3', 'Found 3']]); expect(withScannerValueChanges(resolved, a, b).changes[0].change).toBe('resolved'); expect(withScannerValueChanges(report, null, null).changes).toEqual(report.changes); // A regression reads its passing A record through its counterpart, so identical values are not shown as missing in A. const regressed = { ...report, changes: [{ ...entry(1), change: 'regressed' as const, side: 'target' as const, recordIds: ['b-1'], counterpartIds: ['a-1'] }] } as TimeslotComparisonReport; const same = timeslot('a', [['Synthetic templates', '1', 'Found 1']]); const problem = withScannerValueChanges(regressed, same, timeslot('b', [['Synthetic templates', '1', 'Found 1']])).changes[0].problem; expect(problem?.baseline).toEqual(problem?.target); expect(problem?.baseline.length).toBeGreaterThan(0); }); });