Files
chimera-Public/test/server/ch-0502-update-lifecycle.test.js
T

83 lines
5.7 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 { 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 }); }
});