74 lines
3.7 KiB
TypeScript
74 lines
3.7 KiB
TypeScript
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 current = file('defender-current', 'defender-for-iot-vulnerabilities', '1.1.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', 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: []
|
|
}]);
|
|
});
|
|
});
|