Assessor_2.0/tests/comparison/timeslot-value-comparison.t...

53 lines
3.9 KiB
TypeScript

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);
});
});