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