Files
chimera-Public/test/server/ch-0404-install-orchestrator.test.js

79 lines
6.8 KiB
JavaScript

'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 }); }
});