import { describe, expect, it } from 'vitest'; import type { FindingOccurrence, FindingReview } from '../../src/domain/canonical-model'; import { planReimportReviewTransfer } from '../../src/domain/reimport-review-transfer'; const provenance = { decisionSource: 'source-native' as const, confidence: { level: 'high' as const, score: 1, reasons: [] }, mappingVersion: '1' }; function occurrence(id: string, ruleId: string, asset = 'asset-a'): FindingOccurrence { return { id, definitionId: `definition-${ruleId}`, scanRunId: id.startsWith('old') ? 'old-run' : 'new-run', importFileId: `${id}-file`, sourceIdentity: { tool: { id: 'scanner', name: 'Scanner' }, originalRuleId: ruleId, canonicalRuleId: ruleId, originalTitle: ruleId }, sourcePosition: { rowIndex: 1 }, originalFields: [{ key: 'rule', label: 'Rule', value: ruleId }], fieldMappings: [], severity: { originalValue: 'high', normalizedValue: 'high', provenance }, status: { originalValue: 'failed', normalizedValue: 'failed', provenance }, affectedAsset: { originalValue: asset, normalizedValue: asset, provenance } }; } function review(target: FindingReview['target'], status: FindingReview['workflowStatus'] = 'accepted'): FindingReview { return { id: 'client-review', target, workflowStatus: status, workflowLabel: 'Accepted Risk', priority: 'high', includeInActionPlan: true, owner: 'Synthetic owner', dueAt: '2027-01-01', comment: 'Synthetic client note', prerequisites: 'Synthetic prerequisite', requiredAccess: 'Synthetic access', nextStep: 'Synthetic next step', acceptanceEvidence: 'Synthetic evidence', markers: ['accepted-risk'], createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-02-01T00:00:00Z', revision: 7 }; } describe('reimport review preservation', () => { it('copies every client-owned field to the uniquely matching replacement occurrence', () => { const oldFinding = occurrence('old-1', 'RULE-1'); const newFinding = occurrence('new-1', 'RULE-1'); const source = review({ kind: 'occurrence', occurrenceId: oldFinding.id }); const plan = planReimportReviewTransfer([oldFinding], [newFinding], [source], '2026-03-01T00:00:00Z'); expect(plan.summary).toEqual({ transferred: 1, retainedByStableIdentity: 0, unresolved: 0 }); expect(plan.reviews[0]).toMatchObject({ ...source, id: expect.stringMatching(/^review-reimport-/), target: { kind: 'occurrence', occurrenceId: 'new-1' }, transferredFromReviewId: source.id, transferredAt: '2026-03-01T00:00:00Z' }); }); it('matches the same rule independently per asset', () => { const oldA = occurrence('old-a', 'RULE-1', 'asset-a'); const oldB = occurrence('old-b', 'RULE-1', 'asset-b'); const newA = occurrence('new-a', 'RULE-1', 'asset-a'); const newB = occurrence('new-b', 'RULE-1', 'asset-b'); const plan = planReimportReviewTransfer([oldA, oldB], [newB, newA], [ review({ kind: 'occurrence', occurrenceId: oldA.id }), { ...review({ kind: 'occurrence', occurrenceId: oldB.id }), id: 'client-review-b', owner: 'Synthetic owner B' } ], '2026-03-01T00:00:00Z'); expect(plan.reviews.map(item => [item.target, item.owner])).toEqual(expect.arrayContaining([ [{ kind: 'occurrence', occurrenceId: 'new-a' }, 'Synthetic owner'], [{ kind: 'occurrence', occurrenceId: 'new-b' }, 'Synthetic owner B'] ])); }); it('never guesses across duplicate identities and leaves the original review intact', () => { const oldFinding = occurrence('old-1', 'RULE-1'); const source = review({ kind: 'occurrence', occurrenceId: oldFinding.id }); const plan = planReimportReviewTransfer( [oldFinding], [occurrence('new-1', 'RULE-1'), occurrence('new-2', 'RULE-1')], [source], '2026-03-01T00:00:00Z' ); expect(plan.reviews).toEqual([]); expect(plan.summary.unresolved).toBe(1); expect(source.target).toEqual({ kind: 'occurrence', occurrenceId: 'old-1' }); }); it('keeps definition reviews in place when the stable definition remains available', () => { const source = review({ kind: 'definition', definitionId: 'definition-RULE-1' }); const plan = planReimportReviewTransfer([occurrence('old-1', 'RULE-1')], [occurrence('new-1', 'RULE-1')], [source], '2026-03-01T00:00:00Z'); expect(plan).toMatchObject({ reviews: [], summary: { transferred: 0, retainedByStableIdentity: 1, unresolved: 0 } }); }); });