Assessor_2.0/tests/domain/reimport-review-transfer.te...

70 lines
4.3 KiB
TypeScript

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