221 lines
13 KiB
JavaScript
221 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const { A8_DISTINCT_CONTROL } = require('../../src/services/ps5-native-pkg-builder-provider');
|
|
const { createPs5FpkgDistinctControlInstallGate } = require('../../src/services/ps5-fpkg-distinct-control-install-gate');
|
|
const ACTOR = 'authenticated-session-a';
|
|
|
|
function fixture(overrides = {}) {
|
|
let clock = Date.parse('2026-08-26T16:00:02.000Z');
|
|
let starts = 0;
|
|
const control = {
|
|
status: 'ready', artifactVerified: true, artifactSelection: 'frozen-distribution-artifact',
|
|
titleId: A8_DISTINCT_CONTROL.titleId, contentId: A8_DISTINCT_CONTROL.contentId,
|
|
absolutePath: '/managed/control.pkg',
|
|
size: A8_DISTINCT_CONTROL.selectedArtifact.size,
|
|
sha256: A8_DISTINCT_CONTROL.selectedArtifact.sha256,
|
|
packageSetId: 91, installed: false, launchVerified: false, productionRestorable: false,
|
|
...overrides.control,
|
|
};
|
|
const packageSet = {
|
|
id: 91, platform: 'ps5',
|
|
manifest: {
|
|
controlProfile: A8_DISTINCT_CONTROL.controlProfile,
|
|
title: { platform: 'ps5', titleId: A8_DISTINCT_CONTROL.titleId },
|
|
packageFormat: { container: 'FIH', debug: true, retail: false, licenseFree: true },
|
|
evidence: { immutableArtifact: { sha256: control.sha256, size: control.size, rebuildSubstitutionAllowed: false } },
|
|
},
|
|
components: [{ componentId: 'base', type: 'base', contentId: control.contentId, adapterHint: A8_DISTINCT_CONTROL.adapterHint, size: control.size, sha256: control.sha256, metadata: { absolutePath: '/managed/control.pkg' } }],
|
|
...overrides.packageSet,
|
|
};
|
|
const scan = overrides.scan || {
|
|
scan_id: 'ftp-12-10000', started_at: '2026-08-26T16:00:00.000Z', completed_at: '2026-08-26T16:00:01.000Z',
|
|
root_coverage: [
|
|
{ root: '/user/app/', coverage_state: 'complete', observed_count: 2 },
|
|
{ root: '/mnt/data/app/', coverage_state: 'complete', observed_count: 3 },
|
|
],
|
|
games: [{ game_id: 'PPSA00001' }, { game_id: 'FAKE00000' }], count: 2,
|
|
};
|
|
const gate = createPs5FpkgDistinctControlInstallGate({
|
|
descriptor: A8_DISTINCT_CONTROL,
|
|
now: () => clock,
|
|
ttlMs: 120_000,
|
|
controlStatus: () => control,
|
|
resolvePackageSet: id => Number(id) === packageSet.id ? packageSet : null,
|
|
scanConsole: overrides.scanConsole || (async () => scan),
|
|
startInstall: overrides.startInstall || (async input => {
|
|
starts += 1;
|
|
assert.equal(input.packageSet, packageSet);
|
|
assert.equal(input.titleId, A8_DISTINCT_CONTROL.titleId);
|
|
assert.equal(input.preflightEvidence.schema, A8_DISTINCT_CONTROL.planSchema);
|
|
assert.equal(input.preflightEvidence.titleId, A8_DISTINCT_CONTROL.titleId);
|
|
assert.equal(input.preflightEvidence.targetAbsent, true);
|
|
assert.equal(input.preflightEvidence.coverageComplete, true);
|
|
assert.match(input.preflightEvidence.planDigest, /^[a-f0-9]{64}$/);
|
|
assert.equal(input.preflightEvidence.scan.scanId, 'ftp-12-10000');
|
|
return { id: 'operation-a8-control', status: 'preparing', installed: false };
|
|
}),
|
|
});
|
|
return { gate, control, packageSet, starts: () => starts, advance: ms => { clock += ms; } };
|
|
}
|
|
|
|
test('complete target-absent scan freezes a bounded single-use control install plan', async () => {
|
|
const f = fixture();
|
|
const plan = await f.gate.plan(ACTOR);
|
|
await assert.rejects(() => f.gate.plan('authenticated-session-b'), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_BUSY');
|
|
assert.equal(plan.schema, A8_DISTINCT_CONTROL.planSchema);
|
|
assert.equal(plan.titleId, A8_DISTINCT_CONTROL.titleId);
|
|
assert.equal(plan.packageSetId, 91);
|
|
assert.equal(plan.targetAbsent, true);
|
|
assert.equal(plan.coverageComplete, true);
|
|
assert.equal(plan.grantsInstall, false);
|
|
assert.equal(plan.installed, false);
|
|
assert.equal(plan.launchVerified, false);
|
|
assert.equal(plan.productionRestorable, false);
|
|
assert.equal(plan.confirmation, A8_DISTINCT_CONTROL.confirmation);
|
|
assert.match(plan.planDigest, /^[a-f0-9]{64}$/);
|
|
assert.equal(f.starts(), 0);
|
|
assert.equal(Object.isFrozen(plan), true);
|
|
});
|
|
|
|
test('incomplete coverage and an observed target fail closed before authorization', async () => {
|
|
const partial = fixture({ scan: {
|
|
scan_id: 'ftp-partial', completed_at: '2026-08-26T16:00:00.000Z', games: [], count: 0,
|
|
root_coverage: [{ root: '/user/app/', coverage_state: 'complete' }, { root: '/mnt/data/app/', coverage_state: 'inaccessible' }],
|
|
} });
|
|
await assert.rejects(() => partial.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_SCAN_INCOMPLETE');
|
|
|
|
const present = fixture({ scan: {
|
|
scan_id: 'ftp-present', completed_at: '2026-08-26T16:00:00.000Z', count: 1,
|
|
root_coverage: [{ root: '/user/app/', coverage_state: 'complete' }, { root: '/mnt/data/app/', coverage_state: 'complete' }],
|
|
games: [{ game_id: A8_DISTINCT_CONTROL.titleId }],
|
|
} });
|
|
await assert.rejects(() => present.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_TARGET_PRESENT');
|
|
});
|
|
|
|
test('a parent-proven absent canonical namespace is complete while unproven absence fails closed', async () => {
|
|
const absent = fixture({ scan: {
|
|
scan_id: 'ftp-absent-root', started_at: '2026-08-26T16:00:00.000Z', completed_at: '2026-08-26T16:00:01.000Z', games: [],
|
|
root_coverage: [
|
|
{ root: '/user/app/', coverage_state: 'complete', namespace_state: 'present', coverage_evidence: 'direct-root-listing' },
|
|
{ root: '/mnt/data/app/', coverage_state: 'complete', namespace_state: 'absent', coverage_evidence: 'complete-parent-listing' },
|
|
],
|
|
} });
|
|
const plan = await absent.gate.plan(ACTOR);
|
|
assert.equal(plan.scan.roots[1].namespaceState, 'absent');
|
|
assert.equal(plan.scan.roots[1].coverageEvidence, 'complete-parent-listing');
|
|
|
|
const unproven = fixture({ scan: {
|
|
scan_id: 'ftp-unproven-absent', started_at: '2026-08-26T16:00:00.000Z', completed_at: '2026-08-26T16:00:01.000Z', games: [],
|
|
root_coverage: [
|
|
{ root: '/user/app/', coverage_state: 'complete' },
|
|
{ root: '/mnt/data/app/', coverage_state: 'complete', namespace_state: 'absent' },
|
|
],
|
|
} });
|
|
await assert.rejects(() => unproven.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_SCAN_INCOMPLETE');
|
|
|
|
const ancestorAbsent = fixture({ scan: {
|
|
scan_id: 'ftp-ancestor-absent', started_at: '2026-08-26T16:00:00.000Z', completed_at: '2026-08-26T16:00:01.000Z', games: [],
|
|
root_coverage: [
|
|
{ root: '/user/app/', coverage_state: 'complete', namespace_state: 'present', coverage_evidence: 'direct-root-listing' },
|
|
{ root: '/mnt/data/app/', coverage_state: 'complete', namespace_state: 'absent', coverage_evidence: 'complete-ancestor-listing', evidence_root: '/mnt/', missing_segment: 'data' },
|
|
],
|
|
} });
|
|
const ancestorPlan = await ancestorAbsent.gate.plan(ACTOR);
|
|
assert.equal(ancestorPlan.scan.roots[1].coverageEvidence, 'complete-ancestor-listing');
|
|
assert.equal(ancestorPlan.scan.roots[1].evidenceRoot, '/mnt/');
|
|
assert.equal(ancestorPlan.scan.roots[1].missingSegment, 'data');
|
|
});
|
|
|
|
test('custom or duplicate roots cannot substitute for canonical install-root coverage', async () => {
|
|
const custom = fixture({ scan: {
|
|
scan_id: 'ftp-custom', started_at: '2026-08-26T16:00:00.000Z', completed_at: '2026-08-26T16:00:01.000Z', games: [],
|
|
root_coverage: [{ root: '/user/custom/', coverage_state: 'complete' }],
|
|
} });
|
|
await assert.rejects(() => custom.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_SCAN_SCOPE');
|
|
const duplicate = fixture({ scan: {
|
|
scan_id: 'ftp-duplicate', started_at: '2026-08-26T16:00:00.000Z', completed_at: '2026-08-26T16:00:01.000Z', games: [],
|
|
root_coverage: [{ root: '/user/app/', coverage_state: 'complete' }, { root: '/user/app', coverage_state: 'complete' }, { root: '/mnt/data/app/', coverage_state: 'complete' }],
|
|
} });
|
|
await assert.rejects(() => duplicate.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_SCAN_SCOPE');
|
|
});
|
|
|
|
test('exact digest and confirmation are mandatory and the plan is consumed before install', async () => {
|
|
const f = fixture();
|
|
const plan = await f.gate.plan(ACTOR);
|
|
await assert.rejects(() => f.gate.start({ planDigest: '0'.repeat(64), confirmation: plan.confirmation }, ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_MISMATCH');
|
|
await assert.rejects(() => f.gate.start({ planDigest: plan.planDigest, confirmation: 'INSTALL' }, ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_CONFIRMATION');
|
|
await assert.rejects(() => f.gate.start({ planDigest: plan.planDigest, confirmation: plan.confirmation }, 'authenticated-session-b'), error => error.code === 'PS5_A8_CONTROL_INSTALL_ACTOR_MISMATCH');
|
|
assert.equal(f.starts(), 0);
|
|
|
|
const result = await f.gate.start({ planDigest: plan.planDigest, confirmation: plan.confirmation }, ACTOR);
|
|
assert.equal(result.operationId, 'operation-a8-control');
|
|
assert.equal(result.installed, false);
|
|
assert.equal(result.postScanRequired, true);
|
|
assert.equal(result.launchVerified, false);
|
|
assert.equal(result.productionRestorable, false);
|
|
assert.equal(f.starts(), 1);
|
|
await assert.rejects(() => f.gate.start({ planDigest: plan.planDigest, confirmation: plan.confirmation }, ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_REQUIRED');
|
|
assert.equal(f.starts(), 1);
|
|
});
|
|
|
|
test('expired plans and artifact or package-set drift cannot start installation', async () => {
|
|
const expired = fixture();
|
|
const expiredPlan = await expired.gate.plan(ACTOR);
|
|
expired.advance(120_001);
|
|
await assert.rejects(() => expired.gate.start({ planDigest: expiredPlan.planDigest, confirmation: expiredPlan.confirmation }, ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_EXPIRED');
|
|
|
|
const drift = fixture();
|
|
const driftPlan = await drift.gate.plan(ACTOR);
|
|
drift.control.sha256 = 'f'.repeat(64);
|
|
await assert.rejects(() => drift.gate.start({ planDigest: driftPlan.planDigest, confirmation: driftPlan.confirmation }, ACTOR), error => error.code === 'PS5_A8_CONTROL_ARTIFACT_DRIFT');
|
|
assert.equal(drift.starts(), 0);
|
|
|
|
const boundary = fixture();
|
|
const boundaryPlan = await boundary.gate.plan(ACTOR);
|
|
boundary.advance(120_000);
|
|
await assert.rejects(() => boundary.gate.start({ planDigest: boundaryPlan.planDigest, confirmation: boundaryPlan.confirmation }, ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_EXPIRED');
|
|
});
|
|
|
|
test('an installer failure still consumes the single-use plan', async () => {
|
|
const f = fixture({ startInstall: async () => { throw Object.assign(new Error('provider failed'), { code: 'PROVIDER_FAILED' }); } });
|
|
const plan = await f.gate.plan(ACTOR);
|
|
await assert.rejects(() => f.gate.start({ planDigest: plan.planDigest, confirmation: plan.confirmation }, ACTOR), error => error.code === 'PROVIDER_FAILED');
|
|
await assert.rejects(() => f.gate.start({ planDigest: plan.planDigest, confirmation: plan.confirmation }, ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_REQUIRED');
|
|
});
|
|
|
|
test('unfrozen, unregistered or semantically different package sets are rejected', async () => {
|
|
await assert.rejects(() => fixture({ control: { artifactSelection: 'local-unfrozen-build' } }).gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_NOT_READY');
|
|
await assert.rejects(() => fixture({ packageSet: { manifest: { controlProfile: 'other' } } }).gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_PACKAGE_SET_DRIFT');
|
|
const base = fixture();
|
|
base.packageSet.components[0].metadata.absolutePath = '/managed/different.pkg';
|
|
await assert.rejects(() => base.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_PACKAGE_SET_DRIFT');
|
|
});
|
|
|
|
test('stale, future and reversed scan timestamps fail closed', async () => {
|
|
for (const scan of [
|
|
{ scan_id: 'stale', started_at: '2026-08-26T15:50:00.000Z', completed_at: '2026-08-26T15:50:01.000Z' },
|
|
{ scan_id: 'future', started_at: '2026-08-26T16:01:00.000Z', completed_at: '2026-08-26T16:01:01.000Z' },
|
|
{ scan_id: 'reversed', started_at: '2026-08-26T16:00:01.000Z', completed_at: '2026-08-26T16:00:00.000Z' },
|
|
]) {
|
|
const f = fixture({ scan: { ...scan, games: [], root_coverage: [{ root: '/user/app/', coverage_state: 'complete' }, { root: '/mnt/data/app/', coverage_state: 'complete' }] } });
|
|
await assert.rejects(() => f.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_SCAN_STALE');
|
|
}
|
|
});
|
|
|
|
test('overlapping preflights and start-during-scan are rejected', async () => {
|
|
let release;
|
|
const deferred = new Promise(resolve => { release = resolve; });
|
|
const f = fixture({ scanConsole: () => deferred });
|
|
const pending = f.gate.plan(ACTOR);
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
await assert.rejects(() => f.gate.plan(ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_BUSY');
|
|
await assert.rejects(() => f.gate.start({ planDigest: '0'.repeat(64), confirmation: A8_DISTINCT_CONTROL.confirmation }, ACTOR), error => error.code === 'PS5_A8_CONTROL_INSTALL_PLAN_BUSY');
|
|
release({
|
|
scan_id: 'deferred', started_at: '2026-08-26T16:00:00.000Z', completed_at: '2026-08-26T16:00:01.000Z', games: [],
|
|
root_coverage: [{ root: '/user/app/', coverage_state: 'complete' }, { root: '/mnt/data/app/', coverage_state: 'complete' }],
|
|
});
|
|
assert.match((await pending).planDigest, /^[a-f0-9]{64}$/);
|
|
});
|