import { describe, expect, it } from 'vitest'; import type { ImportFileRecord } from '../../src/domain/canonical-model'; import { processingReadiness, processingScopeIssues, resolvedCompatibilityKey } from '../../src/domain/processing-compatibility'; const confidence = { level: 'high' as const, score: 1, reasons: ['synthetic'] }; function file(id: string, profileId: string, profileVersion: string, mappingVersion: string): ImportFileRecord { return { id, scanRunId: `run-${id}`, fileName: `${id}.json`, format: 'JSON', sizeBytes: 1, sha256: 'a'.repeat(64), importedAt: '2026-08-23T00:00:00.000Z', detectedTool: { id: profileId, name: profileId }, detection: confidence, processedWith: { parserId: 'synthetic', parserVersion: '1.0.0', profileId, profileVersion, mappingVersion }, sourceArtifact: { kind: 'not-retained', reason: 'synthetic' } }; } describe('processing compatibility contracts', () => { it('separates PingCastle presentation from canonical mapping semantics', () => { const old = file('old', 'pingcastle', '1.1.0', '1.1.0'); const contextless = file('contextless', 'pingcastle', '1.2.0', '1.1.0'); const previous = file('previous', 'pingcastle', '1.4.0', '1.2.0'); const current = file('current', 'pingcastle', '1.5.0', '1.2.0'); expect(resolvedCompatibilityKey('pingcastle', old.processedWith)).toBe('pingcastle-canonical-v1.1'); expect(resolvedCompatibilityKey('pingcastle', contextless.processedWith)).toBe('pingcastle-canonical-v1.1'); expect(resolvedCompatibilityKey('pingcastle', current.processedWith)).toBe('pingcastle-canonical-v1.1'); expect(processingReadiness('pingcastle', old.processedWith)).toBe('reimport-recommended'); expect(processingReadiness('pingcastle', contextless.processedWith)).toBe('reimport-recommended'); expect(processingReadiness('pingcastle', previous.processedWith)).toBe('reimport-recommended'); expect(processingReadiness('pingcastle', current.processedWith)).toBe('current'); expect(processingScopeIssues([old, contextless, current])).toEqual([]); }); it('uses the recorded profile when a caller supplies a different detected tool id', () => { const legacy = file('defender-old', 'defender-for-iot-vulnerabilities', '1.0.0', '1.0.0'); const previous = file('defender-previous', 'defender-for-iot-vulnerabilities', '1.1.0', '1.0.0'); const current = file('defender-current', 'defender-for-iot-vulnerabilities', '1.2.0', '1.0.0'); current.processedWith.normalizationVersion = '1.1.0'; current.processedWith.presentationVersion = '1.0.0'; current.processedWith.compatibilityKey = 'defender-for-iot-vulnerabilities-v2'; expect(processingReadiness('microsoft-defender-for-iot', legacy.processedWith)).toBe('incompatible'); expect(processingReadiness('microsoft-defender-for-iot', previous.processedWith)).toBe('reimport-recommended'); expect(processingReadiness('microsoft-defender-for-iot', current.processedWith)).toBe('current'); expect(resolvedCompatibilityKey('microsoft-defender-for-iot', current.processedWith)) .toBe('defender-for-iot-vulnerabilities-v2'); }); it('blocks semantically incompatible versions in one scope', () => { const legacy = file('legacy', 'pingcastle', '1.0.0', '1.0.0'); const current = file('current', 'pingcastle', '1.4.0', '1.2.0'); expect(processingReadiness('pingcastle', legacy.processedWith)).toBe('incompatible'); expect(processingScopeIssues([legacy, current])).toMatchObject([{ toolId: 'pingcastle', code: 'incompatible-processing-contracts' }]); }); it('fails closed for unknown persisted contracts', () => { const unknown = file('unknown', 'pingcastle', '9.9.9', '9.9.9'); expect(processingReadiness('pingcastle', unknown.processedWith)).toBe('unknown'); expect(processingScopeIssues([unknown])).toEqual([{ toolId: 'pingcastle', code: 'unknown-processing-contract', compatibilityKeys: [] }]); }); });