70 lines
4.3 KiB
TypeScript
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 } });
|
|
});
|
|
});
|