Files
chimera-Public/test/server/ch-0407-post-install-verification.test.js
T

108 lines
6.3 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 { 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 });
}
});