Files
chimera-Public/test/server/ch-1107-ps5-fpkg-installer.test.js
T

546 lines
37 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
createPs5UploadPackageInstallerProvider,
PINNED_PS5_PACKAGE_INSTALLER,
} = require('../../src/services/ps5upload-package-installer-provider');
const { createPs5NativePackageInstallService } = require('../../src/services/ps5-native-package-install-service');
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-'));
const filePath = path.join(root, 'EP9000-PPSA01234_00-CHIMERATEST00001.pkg');
const bytes = Buffer.concat([Buffer.from([0x7f, 0x46, 0x49, 0x48]), Buffer.alloc(64, 0x5a)]);
fs.writeFileSync(filePath, bytes);
return {
root,
filePath,
component: {
componentId: 'base', type: 'base', size: bytes.length,
sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
contentId: 'EP9000-PPSA01234_00-CHIMERATEST00001',
metadata: { absolutePath: filePath, titleId: { value: 'PPSA01234' } },
},
};
}
function response(body, status = 200) {
return { ok: status >= 200 && status < 300, status, json: async () => body, text: async () => typeof body === 'string' ? body : JSON.stringify(body) };
}
function activeRuntimeProcesses() {
return response({ processes: [
{ pid: 107, name: 'kstuff.elf', comm: 'kstuff.elf', kind: 'payload' },
{ pid: 108, name: 'etaHEN Utility Daemon', comm: 'etaHEN Utility Daemon', kind: 'system' },
], truncated: false });
}
function exactRuntimeIdentity() {
return {
observed: {
engine: { revision: PINNED_PS5_PACKAGE_INSTALLER.revision, sha256: PINNED_PS5_PACKAGE_INSTALLER.artifacts.engineLinuxX64Sha256, evidence: 'running-process-bytes' },
payload: { revision: PINNED_PS5_PACKAGE_INSTALLER.revision, sha256: PINNED_PS5_PACKAGE_INSTALLER.artifacts.payloadSha256, evidence: 'loader-input-and-instance-transition' },
dpi: { active: false },
},
};
}
test('PS5 FPKG provider pins 5.4.19, accepts only loopback and installs a hash-bound staged FIH .pkg', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const calls = [];
const fetch = async (url, options = {}) => {
calls.push({ url: String(url), options, body: options.body ? JSON.parse(options.body) : null });
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/ps5/status')) return response({ version: '5.4.19', ucred_elevated: true });
if (String(url).includes('/api/ps5/readiness')) return response({ ready: true });
if (String(url).includes('/api/ps5/process/list')) return activeRuntimeProcesses();
return response({ session_id: '7846dc5a-415c-4d72-bf1b-97fd08a887b4', err_code: 0, package_type: 'PS5GD', register_path: 'bgft', launchable: null });
};
const provider = createPs5UploadPackageInstallerProvider({ engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root, getRuntimeIdentity: exactRuntimeIdentity, fetch });
const stagedPath = `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg`;
const result = await provider.start({ component: item.component, stagedPath });
assert.equal(PINNED_PS5_PACKAGE_INSTALLER.version, '5.4.19');
assert.equal(result.installed, false);
assert.equal(result.status, 'installing');
assert.deepEqual(calls.find(call => call.body?.package_type_override).body, {
ps5_addr: 'ps5.local:9114', path: null, split_root: null,
package_type_override: 'PS5GD', local_ps5_path: stagedPath,
content_id: item.component.contentId, expected_size: item.component.size,
package_fingerprint: null, delete_staging: false, serve_only: false,
});
assert.equal(fs.existsSync(item.filePath), true);
assert.throws(() => createPs5UploadPackageInstallerProvider({ engineOrigin: 'http://192.168.1.10:19113', consoleHost: 'ps5.local', packageRoot: item.root, fetch }), error => error.code === 'PS5_PKG_ENGINE_ORIGIN');
});
test('PS5 FPKG provider follows the pinned standalone-DPI fallback and restores the main payload', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const calls = []; let dpiEnsures = 0; let restores = 0;
const provider = createPs5UploadPackageInstallerProvider({
engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root,
getRuntimeIdentity: exactRuntimeIdentity,
ensureDpi: async () => { dpiEnsures += 1; return { ready: true }; },
restorePayload: async () => { restores += 1; return { managementReady: true }; },
fetch: async (url, options = {}) => {
calls.push({ url: String(url), body: options.body ? JSON.parse(options.body) : null });
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/ps5/status')) return response({ version: '5.4.19', ucred_elevated: true });
if (String(url).includes('/api/ps5/readiness')) return response({ ready: true });
if (String(url).includes('/api/ps5/process/list')) return activeRuntimeProcesses();
if (String(url).endsWith('/api/pkg/install/start')) return response({ error: 'payload PKG_INSTALL failed: frame rejected' }, 502);
if (String(url).endsWith('/api/pkg/dpi-install')) return response({ ok: true, rc: 0, init_failed: false });
return response({});
},
});
const result = await provider.start({ component: item.component, stagedPath: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` });
assert.equal(dpiEnsures, 1);
assert.equal(restores, 1);
assert.equal(result.status, 'accepted_unverified');
assert.equal(result.route, 'dpi-staged');
assert.equal(result.installed, false);
assert.equal(result.postScanRequired, true);
assert.deepEqual(calls.find(call => call.url.endsWith('/api/pkg/dpi-install')).body, { ps5_addr: 'ps5.local:9114', local_ps5_path: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` });
});
test('PS5 FPKG provider prefers a verified active etaHEN DPI v2 stream and skips AppInst plus payload swapping', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
let primaryStarts = 0; let dpiEnsures = 0; let restores = 0; let directInstalls = 0;
const dpiV2Installer = {
probe: async () => ({ available: true, protocol: 'etahen-dpi-v2-url-v1' }),
install: async artifact => {
directInstalls += 1;
assert.equal(artifact.filePath, item.filePath);
assert.equal(artifact.sha256, item.component.sha256);
return { accepted: true, installed: false, postScanRequired: true, response: 'SUCCESS: queued', responseStatus: 200, responseSha256: 'b'.repeat(64) };
},
};
const provider = createPs5UploadPackageInstallerProvider({
engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root,
getRuntimeIdentity: exactRuntimeIdentity,
getEtaHenConfig: async () => ({ values: { PS5Debug: '1', DPI: '0', DPI_v2: '1' } }),
dpiV2Installer,
ensureDpi: async () => { dpiEnsures += 1; },
restorePayload: async () => { restores += 1; },
fetch: async url => {
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/ps5/status')) return response({ version: '5.4.19', ucred_elevated: true });
if (String(url).includes('/api/ps5/readiness')) return response({ ready: true });
if (String(url).includes('/api/ps5/process/list')) return activeRuntimeProcesses();
if (String(url).endsWith('/api/pkg/install/start')) primaryStarts += 1;
return response({ session_id: 'should-not-start', err_code: 0 });
},
});
const result = await provider.start({ component: item.component, stagedPath: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` });
assert.equal(result.status, 'accepted_unverified');
assert.equal(result.route, 'etahen-dpi-v2-url');
assert.equal(result.installed, false);
assert.equal(result.postScanRequired, true);
assert.equal(result.providerEvidence.response, 'SUCCESS: queued');
assert.equal(directInstalls, 1);
assert.equal(primaryStarts, 0);
assert.equal(dpiEnsures, 0);
assert.equal(restores, 0);
});
test('PS5 FPKG direct etaHEN DPI v2 preflight does not require PS5Upload engine or management payload', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
let managementCalls = 0;
const dpiV2Installer = {
probe: async () => ({ available: true, protocol: 'etahen-dpi-v2-url-v1' }),
install: async () => ({ accepted: true, installed: false, postScanRequired: true }),
};
const provider = createPs5UploadPackageInstallerProvider({
engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root,
getEtaHenConfig: async () => ({ values: { PS5Debug: '1', DPI: '0', DPI_v2: '1' } }),
dpiV2Installer,
fetch: async url => {
if (String(url).endsWith('/api/version')) return response({ error: 'engine offline' }, 503);
managementCalls += 1;
return response({ error: 'management offline' }, 503);
},
});
const readiness = await provider.preflightInstall({ components: [item.component] });
assert.equal(readiness.ready, true);
assert.equal(readiness.installStrategy, 'etahen-dpi-v2-url');
assert.equal(readiness.dpiV2.available, true);
assert.deepEqual(readiness.capabilities, {
artifactReady: true,
etaHenDpiV2Configured: true,
etaHenDpiV2IdentityReady: true,
});
assert.equal(readiness.runtimeIdentity, null);
assert.equal(readiness.runtimeIdentityRequired, false);
assert.equal(managementCalls, 0);
const result = await provider.start({ component: item.component, stagedPath: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` });
assert.equal(result.route, 'etahen-dpi-v2-url');
});
test('PS5 FPKG provider preserves the upstream error body when no DPI lifecycle is bound', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const provider = createPs5UploadPackageInstallerProvider({
engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root,
getRuntimeIdentity: exactRuntimeIdentity,
fetch: async url => {
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/ps5/status')) return response({ version: '5.4.19', ucred_elevated: true });
if (String(url).includes('/api/ps5/readiness')) return response({ ready: true });
if (String(url).includes('/api/ps5/process/list')) return activeRuntimeProcesses();
return response({ error: 'payload PKG_INSTALL failed: unknown frame' }, 502);
},
});
await assert.rejects(
() => provider.start({ component: item.component, stagedPath: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` }),
error => error.code === 'PS5_PKG_PROVIDER_HTTP' && /unknown frame/.test(error.message) && /unknown frame/.test(error.details?.upstream || ''),
);
});
test('PS5 FPKG provider rejects old pseudo formats, wrong hashes, traversal and unsupported updates', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const provider = createPs5UploadPackageInstallerProvider({ engineOrigin: 'http://localhost:19113', consoleHost: 'ps5.local', packageRoot: item.root, fetch: async () => response({ version: '5.4.19' }) });
const wrongExtension = { ...item.component, metadata: { ...item.component.metadata, absolutePath: path.join(item.root, 'legacy.ffpkg') } };
fs.copyFileSync(item.filePath, wrongExtension.metadata.absolutePath);
const stagedPath = `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg`;
await assert.rejects(() => provider.start({ component: wrongExtension, stagedPath }), error => error.code === 'PS5_PKG_EXTENSION');
await assert.rejects(() => provider.start({ component: { ...item.component, sha256: '0'.repeat(64) }, stagedPath }), error => error.code === 'PS5_FPKG_ARTIFACT_HASH_DRIFT');
await assert.rejects(() => provider.start({ component: { ...item.component, type: 'update' }, stagedPath }), error => error.code === 'PS5_PKG_COMPONENT_TYPE');
const outside = path.join(os.tmpdir(), `outside-${crypto.randomUUID()}.pkg`); fs.copyFileSync(item.filePath, outside); t.after(() => fs.rmSync(outside, { force: true }));
await assert.rejects(() => provider.start({ component: { ...item.component, metadata: { ...item.component.metadata, absolutePath: outside } }, stagedPath }), error => error.code === 'PS5_PKG_PATH');
await assert.rejects(() => provider.start({ component: item.component, stagedPath: '/data/arbitrary.pkg' }), error => error.code === 'PS5_PKG_STAGING_PATH');
});
test('PS5 FPKG provider maps DLC to PS5AC and never equates acceptance with installation', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
let statusBody = { session_id: 's1', phase: 'done', downloaded: 68, total: 68, err_code: 0, accepted_unverified: true, stalled: false, launchable: true };
const calls = [];
const fetch = async (url, options = {}) => {
calls.push({ url: String(url), body: options.body ? JSON.parse(options.body) : null });
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/ps5/status')) return response({ version: '5.4.19', ucred_elevated: true });
if (String(url).includes('/api/ps5/readiness')) return response({ ready: true });
if (String(url).includes('/api/ps5/process/list')) return activeRuntimeProcesses();
if (String(url).includes('/api/pkg/install/status')) return response(statusBody);
return response({ session_id: 's1', err_code: 0, package_type: 'PS5AC' });
};
const provider = createPs5UploadPackageInstallerProvider({ engineOrigin: 'http://[::1]:19113', consoleHost: '10.0.0.20', packageRoot: item.root, getRuntimeIdentity: exactRuntimeIdentity, fetch });
const started = await provider.start({ component: { ...item.component, type: 'dlc' }, stagedPath: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` });
assert.equal(calls.find(call => call.body?.package_type_override).body.package_type_override, 'PS5AC');
assert.equal(started.installed, false);
assert.equal((await provider.status({ sessionId: 's1' })).providerVerified, false);
statusBody = { ...statusBody, accepted_unverified: false };
const complete = await provider.status({ sessionId: 's1' });
assert.equal(complete.providerVerified, true);
assert.equal(complete.installed, false);
assert.equal(complete.postScanRequired, true);
});
test('PS5 FPKG provider fails fast on Sony AppInst rejection and preserves route evidence', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const provider = createPs5UploadPackageInstallerProvider({
engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root,
fetch: async url => {
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/pkg/install/status')) return response({
session_id: 's-appinst', phase: 'install', downloaded: 0, total: item.component.size,
err_code: 0, appinst_err: 0x80B2116F, shellui_err: 0, register_path: 'shellui-rpc',
intdebug_avail: false, kernel_rw: true, installed_bytes: item.component.size - 32_188,
accepted_unverified: false, stalled: false, launchable: null,
});
return response({ ready: true });
},
});
const snapshot = await provider.status({ sessionId: 's-appinst' });
assert.equal(snapshot.phase, 'error');
assert.equal(snapshot.errCode, 0x80B2116F);
assert.match(snapshot.errMessage, /0x80B2116F/);
assert.equal(snapshot.terminalReason, 'sony-appinst-rejected');
assert.equal(snapshot.route, 'shellui-rpc');
assert.equal(snapshot.intdebugAvailable, false);
assert.equal(snapshot.kernelRw, true);
assert.equal(snapshot.appInstError, 0x80B2116F);
assert.equal(snapshot.shellUiError, 0);
assert.equal(snapshot.providerVerified, false);
});
test('PS5 FPKG preflight records etaHEN PS5Debug as diagnostic evidence without treating it as package trust', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
let config = { PS5Debug: '0', DPI: '0', DPI_v2: '0' };
let mutations = 0;
const provider = createPs5UploadPackageInstallerProvider({
engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root,
getEtaHenConfig: async () => ({ values: config }),
fetch: async (url, options = {}) => {
if (options.method === 'POST') mutations += 1;
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/ps5/status')) return response({ version: '5.4.19', ucred_elevated: true, instance_id: '7673539444651544000', started_at_unix: 1786635128, startup_reason: 1 });
if (String(url).includes('/api/ps5/readiness')) return response({ ready: true });
if (String(url).includes('/api/ps5/process/list')) return activeRuntimeProcesses();
return response({});
},
});
const diagnostic = await provider.preflight({ components: [item.component] });
assert.equal(diagnostic.ready, true);
assert.equal(diagnostic.runtime.ps5Debug, false);
assert.equal(diagnostic.runtime.required, false);
assert.equal(diagnostic.runtime.reason, 'ps5debug-disabled-diagnostic-only');
assert.equal(diagnostic.console.instanceId, '7673539444651544000');
assert.equal(diagnostic.console.startedAtUnix, 1786635128);
assert.equal(diagnostic.console.startupReason, 1);
assert.equal(mutations, 0);
config = { ...config, PS5Debug: '1' };
const ready = await provider.preflight({ components: [item.component] });
assert.equal(ready.ready, true);
assert.equal(ready.runtime.ps5Debug, true);
assert.equal(ready.runtime.verified, true);
});
test('PS5 FPKG install job persists terminal provider diagnostics instead of waiting for generic done', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-terminal-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const provider = {
preflight: async () => ({ ready: true }),
start: async () => ({ status: 'installing', sessionId: 's-terminal', errCode: 0 }),
status: async () => ({
sessionId: 's-terminal', phase: 'error', downloaded: 0, total: 68, installedBytes: 36,
errCode: 0x80B2116F, errMessage: 'Sony AppInst rejected the package (0x80B2116F).',
terminalReason: 'sony-appinst-rejected', route: 'shellui-rpc', appInstError: 0x80B2116F,
shellUiError: 0, intdebugAvailable: false, kernelRw: true, providerVerified: false,
acceptedUnverified: false, stalled: false, launchable: null,
}),
cancel: async () => ({ cancelled: true }),
};
const service = createPs5NativePackageInstallService({ provider, stager: async () => ({ stagedPath: '/user/data/ps5upload/pkg_library/test.pkg' }), stateFile: path.join(root, 'jobs.json'), pollMs: 5 });
const operation = await service.start({ titleId: 'PPSA01234', packageSet: { id: 7, components: [{ componentId: 'base', type: 'base', order: 1, sha256: 'a'.repeat(64), size: 68 }] } });
let current = operation;
for (let attempt = 0; attempt < 30 && current.status !== 'blocked'; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 10)); current = service.get(operation.id);
}
assert.equal(current.status, 'blocked');
assert.equal(current.error.code, 'PS5_PKG_INSTALL_REJECTED');
assert.equal(current.error.terminalReason, 'sony-appinst-rejected');
assert.equal(current.error.route, 'shellui-rpc');
assert.equal(current.error.appInstError, 0x80B2116F);
assert.equal(current.error.intdebugAvailable, false);
assert.equal(current.components[0].status, 'rejected');
assert.equal(current.installed, false);
});
test('PS5 FPKG install job performs one deferred DPI fallback after a terminal AppInst rejection', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-deferred-dpi-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
let fallbackCalls = 0;
const provider = {
preflight: async () => ({ ready: true }),
start: async () => ({ status: 'installing', sessionId: 's-primary', errCode: 0 }),
status: async () => ({
sessionId: 's-primary', phase: 'error', reportedPhase: 'install', downloaded: 0, total: 68, installedBytes: 0,
errCode: 0x80B2116F, errMessage: 'Sony AppInst rejected the package (0x80B2116F).',
terminalReason: 'sony-appinst-rejected', route: 'shellui-rpc', appInstError: 0x80B2116F,
shellUiError: 0, intdebugAvailable: false, kernelRw: true, providerVerified: false,
acceptedUnverified: false, stalled: false, launchable: null,
}),
fallback: async ({ stagedPath, primaryEvidence }) => {
fallbackCalls += 1;
assert.equal(stagedPath, '/user/data/ps5upload/pkg_library/test.pkg');
assert.equal(primaryEvidence.appInstError, 0x80B2116F);
return { status: 'accepted_unverified', sessionId: 'dpi-deferred', errCode: 0, route: 'dpi-staged', acceptedUnverified: true, providerVerified: false };
},
cancel: async () => ({ cancelled: true }),
};
const service = createPs5NativePackageInstallService({ provider, stager: async () => ({ stagedPath: '/user/data/ps5upload/pkg_library/test.pkg' }), stateFile: path.join(root, 'jobs.json'), pollMs: 5 });
const operation = await service.start({ titleId: 'PPSA99099', packageSet: { id: 5, components: [{ componentId: 'base', type: 'base', order: 1, sha256: 'c'.repeat(64), size: 68 }] } });
let current = operation;
for (let attempt = 0; attempt < 30 && current.status !== 'awaiting_post_scan'; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 10)); current = service.get(operation.id);
}
assert.equal(current.status, 'awaiting_post_scan');
assert.equal(current.installed, false);
assert.equal(current.components[0].dpiFallbackAttempted, true);
assert.equal(current.components[0].primaryRejection.route, 'shellui-rpc');
assert.equal(current.components[0].route, 'dpi-staged');
assert.equal(fallbackCalls, 1);
});
test('PS5 FPKG install job treats DPI acceptance as unproven until the independent post-scan', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-dpi-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const provider = {
preflight: async () => ({ ready: true }),
start: async () => ({ status: 'accepted_unverified', sessionId: 'dpi-test', errCode: 0, route: 'dpi-staged', acceptedUnverified: true, providerVerified: false }),
status: async () => { throw new Error('DPI acceptance must not use the main-payload status poll'); },
cancel: async () => ({ cancelled: true }),
};
const service = createPs5NativePackageInstallService({ provider, stager: async () => ({ stagedPath: '/user/data/ps5upload/pkg_library/test.pkg' }), stateFile: path.join(root, 'jobs.json'), pollMs: 5 });
const operation = await service.start({ titleId: 'PPSA99099', packageSet: { id: 5, components: [{ componentId: 'base', type: 'base', order: 1, sha256: 'a'.repeat(64), size: 68 }] } });
let current = operation;
for (let attempt = 0; attempt < 30 && current.status !== 'awaiting_post_scan'; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 10)); current = service.get(operation.id);
}
assert.equal(current.status, 'awaiting_post_scan');
assert.equal(current.installed, false);
assert.equal(current.components[0].status, 'accepted_unverified');
assert.equal(current.components[0].providerVerified, false);
assert.equal(current.components[0].route, 'dpi-staged');
});
test('PS5 FPKG direct etaHEN URL handoff skips redundant FTP staging and persists bounded provider evidence', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-direct-url-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
let stages = 0; let stagedPathSeen = 'unset';
const provider = {
preflight: async () => ({ ready: true, runtime: { dpiV2: true }, installStrategy: 'etahen-dpi-v2-url' }),
start: async ({ stagedPath }) => {
stagedPathSeen = stagedPath;
return { status: 'accepted_unverified', sessionId: 'dpi-url', errCode: 0, route: 'etahen-dpi-v2-url', acceptedUnverified: true, providerVerified: false, providerEvidence: { responseStatus: 200, response: 'SUCCESS: queued', responseSha256: 'c'.repeat(64) } };
},
status: async () => { throw new Error('URL handoff must not poll the management payload'); }, cancel: async () => ({ cancelled: true }),
};
const service = createPs5NativePackageInstallService({ provider, stager: async () => { stages += 1; return { stagedPath: '/should/not/be/used.pkg' }; }, stateFile: path.join(root, 'jobs.json') });
const operation = await service.start({ titleId: 'PPSA99099', packageSet: { id: 9, components: [{ componentId: 'base', type: 'base', order: 1, sha256: 'd'.repeat(64), size: 68 }] } });
let current = operation;
for (let attempt = 0; attempt < 30 && current.status !== 'awaiting_post_scan'; attempt += 1) { await new Promise(resolve => setTimeout(resolve, 10)); current = service.get(operation.id); }
assert.equal(current.status, 'awaiting_post_scan');
assert.equal(stages, 0);
assert.equal(stagedPathSeen, null);
assert.equal(current.components[0].providerEvidence.response, 'SUCCESS: queued');
assert.equal(current.installed, false);
});
test('PS5 FPKG install job blocks an identical hash after proven AppInst rejection before staging', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-known-rejection-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const stateFile = path.join(root, 'jobs.json');
const sha256 = 'eafb572e94b72b4b714f8f45a3316abb957fdca4a23ef5c44741c9deb5a16d0c';
const readiness = { ready: true, engine: { actualVersion: '5.2.1' }, runtime: { ps5Debug: false, dpi: false, dpiV2: false }, console: { runtimeVersion: '5.2.1', kernel: '9.60', ucredElevated: true, instanceId: '7673539444651544000', startedAtUnix: 1786635128, startupReason: 1 } };
const runtimeFingerprint = crypto.createHash('sha256').update(JSON.stringify({ engineVersion: '5.2.1', ps5Debug: false, dpi: false, dpiV2: false, managementVersion: '5.2.1', kernel: '9.60', ucredElevated: true, managementInstanceId: '7673539444651544000', managementStartedAtUnix: 1786635128, managementStartupReason: 1, installStrategy: 'primary-then-deferred-dpi-v1' })).digest('hex');
fs.writeFileSync(stateFile, JSON.stringify([{
id: 'previous-rejection', packageSetId: 4, titleId: 'PPSA17330', status: 'blocked', installed: false,
createdAt: 1, updatedAt: 2, runtimeFingerprint,
components: [{ componentId: 'base', type: 'base', order: 0, sha256, size: 1358617285, installed: false }],
error: { code: 'PS5_PKG_INSTALL_REJECTED', terminalReason: 'sony-appinst-rejected', appInstError: 0x80B2116F, route: 'shellui-rpc' },
}]));
let preflights = 0; let stages = 0;
const provider = {
preflight: async () => { preflights += 1; return readiness; },
start: async () => ({ status: 'installing', sessionId: 'should-not-start', errCode: 0 }),
status: async () => ({ phase: 'install' }), cancel: async () => ({ cancelled: true }),
};
const service = createPs5NativePackageInstallService({ provider, stager: async () => { stages += 1; return { stagedPath: '/user/data/ps5upload/pkg_library/test.pkg' }; }, stateFile });
await assert.rejects(
() => service.start({ titleId: 'PPSA17330', packageSet: { id: 4, components: [{ componentId: 'base', type: 'base', order: 1, sha256, size: 1358617285 }] } }),
error => error.code === 'PS5_PKG_INSTALL_KNOWN_REJECTED'
&& error.details?.previousOperationId === 'previous-rejection'
&& error.details?.appInstError === 0x80B2116F,
);
assert.equal(preflights, 1);
assert.equal(stages, 0);
});
test('PS5 FPKG install job permits one retry after a new management instance and blocks a repeat in that instance', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-runtime-retry-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const stateFile = path.join(root, 'jobs.json'); const sha256 = 'b'.repeat(64);
const previousRuntimeFingerprint = crypto.createHash('sha256').update(JSON.stringify({ engineVersion: '5.2.1', ps5Debug: true, dpi: true, dpiV2: false, managementVersion: '5.2.1', kernel: '9.60', ucredElevated: true, managementInstanceId: '111', managementStartedAtUnix: 1786635000, managementStartupReason: 1, installStrategy: 'primary-then-deferred-dpi-v1' })).digest('hex');
fs.writeFileSync(stateFile, JSON.stringify([{
id: 'previous-instance-rejection', packageSetId: 5, titleId: 'PPSA99099', status: 'blocked', installed: false,
createdAt: 1, updatedAt: 2, runtimeFingerprint: previousRuntimeFingerprint, components: [{ componentId: 'base', type: 'base', order: 0, sha256, size: 68, installed: false }],
error: { code: 'PS5_PKG_INSTALL_REJECTED', terminalReason: 'sony-appinst-rejected', appInstError: 0x80B2116F, route: 'shellui-rpc' },
}]));
let stages = 0;
const provider = {
preflight: async () => ({ ready: true, engine: { actualVersion: '5.2.1' }, runtime: { ps5Debug: true, dpi: true, dpiV2: false }, console: { runtimeVersion: '5.2.1', kernel: '9.60', ucredElevated: true, instanceId: '222', startedAtUnix: 1786635128, startupReason: 1 } }),
start: async () => ({ status: 'rejected', sessionId: 'runtime-retry', errCode: 0x80B2116F, errMessage: 'rejected', terminalReason: 'sony-appinst-rejected', route: 'shellui-rpc', appInstError: 0x80B2116F }),
status: async () => ({ phase: 'error' }), cancel: async () => ({ cancelled: true }),
};
const service = createPs5NativePackageInstallService({ provider, stager: async () => { stages += 1; return { stagedPath: '/user/data/ps5upload/pkg_library/test.pkg' }; }, stateFile, pollMs: 5 });
const operation = await service.start({ titleId: 'PPSA99099', packageSet: { id: 5, components: [{ componentId: 'base', type: 'base', order: 1, sha256, size: 68 }] } });
assert.match(operation.runtimeFingerprint, /^[a-f0-9]{64}$/);
let current = operation;
for (let attempt = 0; attempt < 20 && current.status !== 'blocked'; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 5)); current = service.get(operation.id);
}
assert.equal(current.status, 'blocked');
assert.equal(stages, 1);
await assert.rejects(
() => service.start({ titleId: 'PPSA99099', packageSet: { id: 5, components: [{ componentId: 'base', type: 'base', order: 1, sha256, size: 68 }] } }),
error => error.code === 'PS5_PKG_INSTALL_KNOWN_REJECTED' && error.details?.previousOperationId === operation.id,
);
assert.equal(stages, 1);
});
test('PS5 FPKG provider blocks a non-pinned engine before mutation', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
let mutations = 0;
const provider = createPs5UploadPackageInstallerProvider({ engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root, fetch: async (_url, options = {}) => { if (options.method === 'POST') mutations += 1; return response({ version: '5.1.0' }); } });
await assert.rejects(() => provider.start({ component: item.component, stagedPath: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` }), error => error.code === 'PS5_PKG_ENGINE_VERSION');
assert.equal(mutations, 0);
});
test('PS5 FPKG provider requires live management readiness before install start', async t => {
const item = fixture(); t.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
let installStarts = 0;
const provider = createPs5UploadPackageInstallerProvider({ engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: item.root, fetch: async (url, options = {}) => {
if (String(url).endsWith('/api/version')) return response({ version: '5.4.19' });
if (String(url).includes('/api/ps5/readiness')) return response({ ready: false, detail: 'console busy' });
if (options.method === 'POST') installStarts += 1;
return response({});
} });
await assert.rejects(() => provider.start({ component: item.component, stagedPath: `/user/data/ps5upload/pkg_library/${item.component.contentId}.pkg` }), error => error.code === 'PS5_PKG_CONSOLE_NOT_READY');
assert.equal(installStarts, 0);
});
test('PS5 FPKG install job sequences base before DLC and stops at the independent post-scan gate', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps5-fpkg-job-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const started = [];
const provider = {
preflight: async ({ components }) => ({ ready: components.length === 2 }),
start: async ({ component }) => { started.push(component.type); return { status: 'installing', sessionId: `session-${component.type}`, errCode: 0 }; },
status: async ({ sessionId }) => ({ sessionId, phase: 'done', downloaded: 100, total: 100, installedBytes: 100, errCode: 0, errMessage: '', detail: '', providerVerified: true, acceptedUnverified: false, stalled: false, launchable: true }),
cancel: async () => ({ cancelled: true }),
};
const service = createPs5NativePackageInstallService({ provider, stager: async ({ component }) => ({ stagedPath: `/user/data/ps5upload/pkg_library/${component.type}.pkg` }), stateFile: path.join(root, 'jobs.json'), pollMs: 5 });
const operation = await service.start({ titleId: 'PPSA01234', packageSet: { id: 7, components: [{ componentId: 'dlc', type: 'dlc', order: 20, sha256: 'b'.repeat(64), size: 2 }, { componentId: 'base', type: 'base', order: 10, sha256: 'a'.repeat(64), size: 1 }] } });
let current = operation;
for (let attempt = 0; attempt < 30 && current.status !== 'awaiting_post_scan'; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 10)); current = service.get(operation.id);
}
assert.deepEqual(started, ['base', 'dlc']);
assert.equal(current.status, 'awaiting_post_scan');
assert.equal(current.installed, false);
assert.equal(current.components.every(component => component.providerVerified && component.installed === false), true);
const completed = service.markPostScan(operation.id, { source: 'post-install-console-scan' });
assert.equal(completed.status, 'installed_observed');
assert.equal(completed.installed, true);
assert.equal(completed.installedContentVerified, false);
assert.equal(completed.launchVerified, false);
assert.equal(completed.productionRestorable, false);
});
test('server, Electron, web and PS5 workspace expose the guarded .pkg install lifecycle', () => {
const root = path.resolve(__dirname, '../..');
const registry = fs.readFileSync(path.join(root, 'src/server/create-registry.js'), 'utf8');
const preload = fs.readFileSync(path.join(root, 'preload.js'), 'utf8');
const web = fs.readFileSync(path.join(root, 'renderer/pork-web-shim.js'), 'utf8');
const modal = fs.readFileSync(path.join(root, 'renderer/modules/modal.js'), 'utf8');
const html = fs.readFileSync(path.join(root, 'renderer/index.html'), 'utf8');
const dockerfile = fs.readFileSync(path.join(root, 'Dockerfile'), 'utf8');
for (const command of ['ps5-native-packages:restore-plan', 'ps5-native-packages:restore', 'ps5-native-packages:restore-status', 'ps5-native-packages:restore-cancel', 'ps5-native-packages:restore-verify']) assert.match(registry, new RegExp(command));
assert.match(registry, /confirmation: 'INSTALL PS5 DEBUG PACKAGE'/);
assert.match(registry, /\^\[A-Za-z0-9_-\]\{1,64\}\$/);
assert.match(registry, /await hashFile\(localPath\) !== component\.sha256/);
assert.match(registry, /PS5_FPKG_ARTIFACT_HASH_DRIFT/);
assert.match(registry, /ci\.last_seen_at AS observed_at/);
assert.doesNotMatch(registry, /ci\.observed_at/);
assert.match(preload, /ps5NativePackageRestoreStatus/);
assert.match(web, /ps5NativePackageRestoreStatus/);
assert.match(web, /ps5NativePackageRestoreList/);
assert.match(modal, /monitorPs5NativePackageInstall/);
assert.match(modal, /item\.titleId === 'FAKE00000'/);
assert.match(modal, /INSTALL PS5 DEBUG PACKAGE/);
assert.match(modal, /operationId: ps5NativeInstallOperationId/);
assert.match(modal, /ps5NativePackageSetOptionLabel/);
assert.match(html, /id="ps5-native-package-cancel"[^>]*disabled/);
assert.match(dockerfile, /LIBPROSPEROPKG_COMMIT=9084c0e88cd8d7cd9c0b353b8b6b89f7d1cc4275/);
assert.match(dockerfile, /--self-contained true/);
assert.match(dockerfile, /\/out\/chimera-prospero-bridge probe --json/);
assert.match(registry, /ps5\.nativePackageBuilderPath', ''\) \|\| '\/opt\/chimera-prospero-bridge'/);
});