import { describe, expect, it } from 'vitest'; import type { ImportFileRecord } from '../../src/domain/canonical-model'; import { summarizeClientProcessingReadiness, summarizeScanProcessingReadiness } from '../../src/features/project-import/datahub-processing-readiness'; function file(scanRunId: string, profileId: string, profileVersion: string, compatibilityKey: string): ImportFileRecord { return { id: `${scanRunId}-${profileVersion}`, scanRunId, fileName: 'synthetic.html', format: 'HTML', sizeBytes: 10, importedAt: '2026-08-22T08:00:00.000Z', detectedTool: { id: profileId, name: profileId }, detection: { level: 'high', score: 1, reasons: [] }, processedWith: { parserId: 'synthetic', parserVersion: '1.0.0', profileId, profileVersion, mappingVersion: profileVersion, normalizationVersion: profileVersion, presentationVersion: profileVersion, compatibilityKey }, sourceArtifact: { kind: 'not-retained', reason: 'synthetic' } }; } function withDetectedTool(fileRecord: ImportFileRecord, id: string): ImportFileRecord { return { ...fileRecord, detectedTool: { id, name: 'Microsoft Defender for IoT' } }; } describe('Datahub client processing readiness', () => { it('marks current processing contracts green', () => { expect(summarizeClientProcessingReadiness( [file('run-current', 'generic', '1.0.0', 'generic-canonical-v1')], ['run-current'] )).toEqual({ state: 'ready', label: 'Processing ready across all client timeslots.' }); }); it('assesses the stored profile contract independently from the detected tool identity', () => { const defender = withDetectedTool( file('run-defender', 'defender-for-iot-vulnerabilities', '1.1.0', 'defender-for-iot-vulnerabilities-v2'), 'microsoft-defender-for-iot' ); defender.processedWith.mappingVersion = '1.0.0'; defender.processedWith.presentationVersion = '1.0.0'; expect(summarizeScanProcessingReadiness([defender])).toEqual({ state: 'ready', statuses: ['current'], explanation: 'The scan uses a current or compatible ASSESSOR processing profile.' }); }); it('marks reimport recommendations as a yellow update notice', () => { expect(summarizeClientProcessingReadiness( [file('run-old', 'pingcastle', '1.1.0', 'pingcastle-canonical-v1.1')], ['run-old'] )).toEqual({ state: 'update', label: 'ASSESSOR processing profile update available in a client timeslot.' }); expect(summarizeScanProcessingReadiness([ file('run-old', 'pingcastle', '1.1.0', 'pingcastle-canonical-v1.1') ])).toMatchObject({ state: 'update', explanation: 'ASSESSOR profile update: 1.1.0 → 1.4.0. Reimporting applies the newer ASSESSOR processing profile; the pingcastle report itself has not changed. The newer ASSESSOR profile captures additional environment context for the assessment.' }); }); it('marks contextless PingCastle 1.2 imports for reprocessing', () => { expect(summarizeScanProcessingReadiness([ file('run-contextless', 'pingcastle', '1.2.0', 'pingcastle-canonical-v1.1') ])).toMatchObject({ state: 'update', explanation: 'ASSESSOR profile update: 1.2.0 → 1.4.0. Reimporting applies the newer ASSESSOR processing profile; the pingcastle report itself has not changed. The newer ASSESSOR profile captures additional environment context for the assessment.' }); }); it('reserves the error state for incompatible or unknown processing contracts', () => { expect(summarizeClientProcessingReadiness( [file('run-incompatible', 'pingcastle', '1.0.0', 'pingcastle-canonical-v1')], ['run-incompatible'] )).toEqual({ state: 'error', label: 'Processing error requires attention in a client timeslot.' }); expect(summarizeScanProcessingReadiness([ file('run-unknown', 'unknown-scanner', '1.0.0', 'unknown-v1') ])).toMatchObject({ state: 'error' }); }); it('keeps the client yellow while any active older timeslot requires reimport', () => { expect(summarizeClientProcessingReadiness( [ file('run-old', 'pingcastle', '1.1.0', 'pingcastle-canonical-v1.1'), file('run-current', 'generic', '1.0.0', 'generic-canonical-v1') ], ['run-current', 'run-old'] )).toEqual({ state: 'update', label: 'ASSESSOR processing profile update available in a client timeslot.' }); }); it('shows no point when the client has no scanner-backed timeslot', () => { expect(summarizeClientProcessingReadiness([], [])).toEqual({ state: 'no-data', label: 'No processing status available.' }); }); });