'use strict'; const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const test = require('node:test'); const { createInstallOrchestratorService } = require('../../src/services/install-orchestrator-service'); const { createPostInstallVerificationService } = require('../../src/services/post-install-verification-service'); const HASH = 'ab'.repeat(32); const job = () => ({ id: 'job-verify', recipeSnapshot: { recipeSha256: 'cd'.repeat(32), recipe: { orderedSteps: [{ id: 'base-step', componentId: 'base', required: true }, { id: 'update-step', componentId: 'update', required: true }] } }, packageSet: { components: [ { componentId: 'base', type: 'base', version: '1.00', metadata: { titleId: { value: 'CUSA12345' } } }, { componentId: 'update', type: 'update', version: '1.02', metadata: { titleId: { value: 'CUSA12345' } } }, ] }, target: { consoleProfileId: 'living-room', volumeId: 'console-m2' }, }); function scannerWith(response, queries = []) { return { query: async query => { queries.push(query); return structuredClone(response); } }; } const scan = installs => ({ available: true, coverageComplete: true, scanRunId: 77, completedAt: 1234, installs }); const observed = overrides => ({ titleId: 'CUSA12345', version: '1.02', volumeId: 'console-m2', installState: 'observed', location: '/mnt/ext0/CUSA12345', evidence: { provider: 'ftp', scanRunId: 77 }, ...overrides }); test('confirmed evidence is recipe-specific and is the only result marked installed', async () => { const queries = []; const service = createPostInstallVerificationService({ scanner: scannerWith(scan([observed()]), queries) }); const result = await service.verify({ job: job() }); assert.equal(result.verified, true); assert.equal(result.installState, 'installed'); assert.equal(result.outcome, 'confirmed'); assert.deepEqual(queries[0].titles, [{ titleId: 'CUSA12345', version: '1.02' }]); assert.equal(queries[0].recipeSha256, 'cd'.repeat(32)); assert.equal(result.matches[0].observed.evidence.scanRunId, 77); }); test('missing, wrong-version and wrong-volume scans retain exact needs-attention evidence', async () => { const scenarios = [ { installs: [], kind: 'missing' }, { installs: [observed({ version: '1.01' })], kind: 'wrong_version' }, { installs: [observed({ volumeId: 'console-usb' })], kind: 'wrong_volume' }, ]; for (const scenario of scenarios) { const result = await createPostInstallVerificationService({ scanner: scannerWith(scan(scenario.installs)) }).verify({ job: job() }); assert.equal(result.verified, false); assert.equal(result.installState, 'needs_attention'); assert.equal(result.mismatches[0].kind, scenario.kind); assert.ok(result.mismatches[0].observed); assert.match(result.evidenceDigest, /^[a-f0-9]{64}$/); } }); test('unavailable or incomplete scan coverage never confirms installation', async () => { for (const response of [{ available: false, reason: 'console offline' }, { available: true, coverageComplete: false, installs: [observed()] }]) { const result = await createPostInstallVerificationService({ scanner: scannerWith(response) }).verify({ job: job() }); assert.equal(result.verified, false); assert.equal(result.outcome, 'scan_unavailable'); assert.equal(result.mismatches[0].kind, 'scan_unavailable'); assert.notEqual(result.installState, 'installed'); } }); test('rerun from needs-attention repeats only final reconciliation and is idempotent after confirmation', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ch0407-')); let response = scan([]); let executions = 0; let scans = 0; try { const finalVerification = createPostInstallVerificationService({ scanner: { query: async () => { scans += 1; return structuredClone(response); } } }); const adapter = { probe: async () => ({ available: true, version: 'sim' }), execute: async () => { executions += 1; return { accepted: true }; }, verify: async () => ({ verified: true }) }; const service = createInstallOrchestratorService({ storageFile: path.join(root, 'journal.json'), adapters: { 'install-package': adapter }, artifactStore: { verify: async component => ({ verified: true, sha256: component.sha256, size: component.size }) }, recovery: { prepare: async () => ({ id: 'recovery-1', retained: true }) }, finalVerification, uuid: () => 'job-rerun', }); const packageSet = { id: 7, platform: 'ps4', region: 'US', components: [ { componentId: 'base', type: 'base', required: true, version: '1.00', sha256: HASH, size: 10, contentId: 'UP0001-CUSA12345_00-BASEPACKAGE000001', region: 'US', metadata: { platform: 'ps4', titleId: { value: 'CUSA12345' } } }, { componentId: 'update', type: 'update', required: true, version: '1.02', sha256: HASH, size: 5, contentId: 'UP0001-CUSA12345_00-UPDATEPACKAGE001', region: 'US', metadata: { platform: 'ps4', titleId: { value: 'CUSA12345' } } }, ], dependencies: [{ componentId: 'update', dependsOnComponentId: 'base', kind: 'requires' }] }; const recipe = { schemaVersion: 1, recipeVersion: 1, packageSetId: 7, notes: '', steps: [ { id: 'base-step', componentId: 'base', action: 'install-package', required: true, dependsOn: [], destinationCapabilities: [] }, { id: 'update-step', componentId: 'update', action: 'install-package', required: true, dependsOn: ['base-step'], destinationCapabilities: [] }, ] }; const target = { freeBytes: 1000, volumeId: 'console-m2', consoleProfileId: 'living-room', destinationCapabilities: { canInstall: true, canCopy: true, canMount: true, canRead: true, stagingPatch: true } }; const created = await service.create({ recipe, packageSet, target, expectedRecipeVersion: 1, idempotencyKey: 'verify-rerun' }); const first = await service.run(created.job.id); assert.equal(first.status, 'needs_attention'); assert.equal(first.finalEvidence.mismatches[0].kind, 'missing'); response = scan([observed()]); const second = await service.run(created.job.id); assert.equal(second.status, 'completed'); assert.equal(second.finalEvidence.installState, 'installed'); const third = await service.run(created.job.id); assert.deepEqual(third, second); assert.equal(executions, 2); assert.equal(scans, 2); } finally { fs.rmSync(root, { recursive: true, force: true }); } });