'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 { createUpdateLifecycleService } = require('../../src/services/update-lifecycle-service'); const SHA = 'ab'.repeat(32); function fixture(root, overrides = {}) { const calls = []; let scan = { available: true, coverageComplete: true, scanRunId: 9, installs: [{ titleId: 'CUSA12345', version: '1.02', volumeId: 'm2', installState: 'observed' }] }; const values = { storageFile: path.join(root, 'updates.json'), downloader: { probe: async () => ({ available: true, verifiedHash: true }), download: async () => { calls.push('download'); return { verified: true, sha256: SHA, bytes: 100, managedPath: 'updates/CUSA12345.pkg' }; } }, stager: { probe: async () => ({ available: true, capabilities: { hashBoundStage: true } }), stage: async input => { calls.push('stage'); return { staged: true, sha256: input.downloadEvidence.sha256, stagingRef: 'console-staging-1' }; } }, installers: { 'ps4-dpi': { probe: async () => ({ available: true, id: 'ps4-dpi', platform: 'ps4', capabilities: { installRequest: true, idempotency: true, postScanRequired: true } }), requestInstall: async () => { calls.push('install'); return { accepted: true, handoffId: 'dpi-1', installed: false }; } } }, scanner: { query: async () => { calls.push('scan'); return structuredClone(scan); } }, preflight: { check: async () => ({ ok: true, checks: ['hash', 'space', 'service'] }) }, uuid: () => 'update-job-1', ...overrides, }; return { values, calls, setScan: value => { scan = value; } }; } function input() { return { idempotencyKey: 'official-CUSA12345-1.02-m2', titleId: 'CUSA12345', expectedVersion: '1.02', platform: 'ps4', requiredAdapter: 'ps4-dpi', expectedSha256: SHA, target: { consoleProfileId: 'living-room', volumeId: 'm2' }, officialEvidence: { source: 'sony-official', observedAt: 1 } }; } test('download-only retains exact hash evidence without staging or installing', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-update-download-')); try { const f = fixture(root); const service = createUpdateLifecycleService(f.values); const created = service.create(input()); const downloaded = await service.download(created.job.id); assert.equal(downloaded.status, 'downloaded'); assert.equal(downloaded.downloadEvidence.sha256, SHA); assert.equal(downloaded.downloadEvidence.bytes, 100); assert.deepEqual(f.calls, ['download']); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('stage-only remains distinct and binds staging evidence to the download hash', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-update-stage-')); try { const f = fixture(root); const service = createUpdateLifecycleService(f.values); const id = service.create(input()).job.id; await service.download(id); const staged = await service.stage(id); assert.equal(staged.status, 'staged'); assert.equal(staged.stageEvidence.sha256, staged.downloadEvidence.sha256); assert.deepEqual(f.calls, ['download', 'stage']); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('install failure remains needs-attention and retry reuses the idempotency contract', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-update-install-fail-')); let attempts = 0; try { const f = fixture(root); f.values.installers['ps4-dpi'].requestInstall = async input => { f.calls.push(input.idempotencyKey); attempts += 1; if (attempts === 1) throw new Error('simulated installer failure'); return { accepted: true }; }; const service = createUpdateLifecycleService(f.values); const id = service.create(input()).job.id; await service.download(id); await service.stage(id); const failed = await service.requestInstall(id); assert.equal(failed.status, 'needs_attention'); assert.equal(failed.failedPhase, 'install'); const retried = await service.retry(id); assert.equal(retried.status, 'verifying'); assert.deepEqual(f.calls.slice(-2), ['official-CUSA12345-1.02-m2:install', 'official-CUSA12345-1.02-m2:install']); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('restart preserves verifying state and only exact post-scan version evidence becomes verified', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-update-verify-')); try { const firstFixture = fixture(root); const first = createUpdateLifecycleService(firstFixture.values); const id = first.create(input()).job.id; await first.download(id); await first.stage(id); const requested = await first.requestInstall(id); assert.equal(requested.status, 'verifying'); const secondFixture = fixture(root); secondFixture.setScan({ available: true, coverageComplete: true, scanRunId: 10, installs: [{ titleId: 'CUSA12345', version: '1.01', volumeId: 'm2', installState: 'observed' }] }); const restored = createUpdateLifecycleService(secondFixture.values); assert.equal(restored.get(id).status, 'verifying'); const mismatch = await restored.verify(id); assert.equal(mismatch.status, 'needs_attention'); assert.equal(mismatch.finalEvidence.installs[0].version, '1.01'); secondFixture.setScan({ available: true, coverageComplete: true, scanRunId: 11, installs: [{ titleId: 'CUSA12345', version: '1.02', volumeId: 'm2', installState: 'observed' }] }); const verified = await restored.retry(id); assert.equal(verified.status, 'verified'); assert.equal(verified.finalEvidence.scanRunId, 11); assert.deepEqual(secondFixture.calls, ['scan', 'scan']); } finally { fs.rmSync(root, { recursive: true, force: true }); } });