'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 component = (componentId, type, size, hash, required = true) => ({ componentId, type, size, sha256: hash.repeat(64), required, version: type === 'update' ? '1.02' : '1.00', contentId: `UP0001-CUSA12345_00-${componentId.toUpperCase().padEnd(16, 'X')}`, region: 'US', metadata: { platform: 'ps4', titleId: { value: 'CUSA12345' } } }); const packageSet = () => ({ id: 1, platform: 'ps4', region: 'US', components: [component('base', 'base', 4, '1'), component('update', 'update', 2, '2')], dependencies: [{ componentId: 'update', dependsOnComponentId: 'base', kind: 'requires' }] }); const recipe = () => ({ schemaVersion: 1, recipeVersion: 1, packageSetId: 1, notes: 'simulator only', steps: [{ id: 'base', componentId: 'base', action: 'install-package', required: true, dependsOn: [], destinationCapabilities: [] }, { id: 'update', componentId: 'update', action: 'install-package', required: true, dependsOn: ['base'], destinationCapabilities: [] }] }); const target = () => ({ consoleProfileId: 'simulator', volumeId: 'internal', freeBytes: 100, destinationCapabilities: { canInstall: true, canCopy: true, canMount: true, canRead: true, stagingPatch: true } }); function fixtures(root, overrides = {}) { const executions = []; const adapter = { probe: async () => ({ available: true, version: 'sim-1' }), execute: async input => { executions.push(input.component.componentId); return { staged: input.component.componentId }; }, verify: async input => ({ verified: true, observed: input.component.componentId }) }; return { storageFile: path.join(root, 'jobs.json'), adapters: { 'install-package': adapter }, artifactStore: { verify: async item => ({ verified: true, sha256: item.sha256, size: item.size }) }, recovery: { prepare: async ({ jobId }) => ({ id: `recovery-${jobId}`, retained: true }) }, finalVerification: { verify: async () => ({ verified: true, scanId: 'scan-1' }) }, audit() {}, uuid: () => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', executions, ...overrides }; } test('restart preserves completed verified children and resumes only remaining work', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-orchestrator-resume-')); let interrupted = false; try { const firstFixtures = fixtures(root, { hooks: { afterChildVerified: async (_job, child) => { if (child.stepId === 'base') { interrupted = true; throw Object.assign(new Error('power loss'), { code: 'PROCESS_INTERRUPTED' }); } } } }); const first = createInstallOrchestratorService(firstFixtures); const created = await first.create({ recipe: recipe(), packageSet: packageSet(), target: target(), expectedRecipeVersion: 1, idempotencyKey: 'install-one' }); await assert.rejects(() => first.run(created.job.id), error => error.code === 'PROCESS_INTERRUPTED'); assert.equal(interrupted, true); const secondFixtures = fixtures(root); const second = createInstallOrchestratorService(secondFixtures); assert.equal(second.get(created.job.id).status, 'interrupted'); assert.equal(second.get(created.job.id).children[0].status, 'completed'); second.resume(created.job.id); const completed = await second.run(created.job.id); assert.equal(completed.status, 'completed'); assert.deepEqual(secondFixtures.executions, ['update']); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('child failure blocks dependents and preserves recovery evidence', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-orchestrator-fail-')); try { const values = fixtures(root); values.adapters['install-package'].execute = async input => { if (input.component.componentId === 'base') throw new Error('simulated adapter failure'); return {}; }; const service = createInstallOrchestratorService(values); const created = await service.create({ recipe: recipe(), packageSet: packageSet(), target: target(), expectedRecipeVersion: 1, idempotencyKey: 'install-fail' }); const failed = await service.run(created.job.id); assert.equal(failed.status, 'failed'); assert.equal(failed.children[0].status, 'failed'); assert.equal(failed.children[1].status, 'blocked'); assert.match(failed.recovery.id, /^recovery-/); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('idempotency deduplicates parents and final success waits for verification', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-orchestrator-idem-')); let finalChecks = 0; try { const values = fixtures(root, { finalVerification: { verify: async () => { finalChecks += 1; return { verified: false }; } } }); const service = createInstallOrchestratorService(values); const input = { recipe: recipe(), packageSet: packageSet(), target: target(), expectedRecipeVersion: 1, idempotencyKey: 'same-operation' }; const first = await service.create(input); const second = await service.create(input); assert.equal(second.deduplicated, true); assert.equal(second.job.id, first.job.id); const result = await service.run(first.job.id); assert.equal(result.status, 'needs_attention'); assert.equal(finalChecks, 1); assert.equal(values.executions.length, 2); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('preflight blocks bad hashes/space/services and cancel keeps recovery', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-orchestrator-preflight-')); try { await assert.rejects(() => createInstallOrchestratorService(fixtures(root, { artifactStore: { verify: async item => ({ verified: true, sha256: '0'.repeat(64), size: item.size }) } })).create({ recipe: recipe(), packageSet: packageSet(), target: target(), expectedRecipeVersion: 1, idempotencyKey: 'bad-hash' }), error => error.code === 'INSTALL_ARTIFACT_INVALID'); await assert.rejects(() => createInstallOrchestratorService(fixtures(root)).create({ recipe: recipe(), packageSet: packageSet(), target: { ...target(), freeBytes: 1 }, expectedRecipeVersion: 1, idempotencyKey: 'no-space' }), error => error.code === 'INSTALL_SPACE'); const service = createInstallOrchestratorService(fixtures(root)); const created = await service.create({ recipe: recipe(), packageSet: packageSet(), target: target(), expectedRecipeVersion: 1, idempotencyKey: 'cancel-safe' }); const cancelled = await service.cancel(created.job.id); assert.equal(cancelled.status, 'cancelled'); assert.equal(cancelled.recovery.retained, true); assert.equal(cancelled.children.every(child => child.status === 'blocked'), true); } finally { fs.rmSync(root, { recursive: true, force: true }); } });