194 lines
11 KiB
JavaScript
194 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
const {
|
|
BASE_REVISION,
|
|
IDENTITY,
|
|
SAMPLE_OFFSETS_MS,
|
|
SCHEMA,
|
|
createPs5ShellUiTransitionHostPlan,
|
|
normalizeShellUiTransitionObservation,
|
|
} = require('../../src/services/ps5-shellui-transition-observation');
|
|
const {
|
|
P15_SHELLUI_TRANSITION_DIAGNOSTIC,
|
|
createPs5UploadPackageInstallerProvider,
|
|
} = require('../../src/services/ps5upload-package-installer-provider');
|
|
|
|
const OPERATION_ID = '123e4567-e89b-42d3-a456-426614174000';
|
|
const TITLE_ID = 'CHIM00003';
|
|
|
|
function attempt() {
|
|
return {
|
|
index: 1,
|
|
path: 'shellui_rpc',
|
|
call_entered: true,
|
|
dispatched: true,
|
|
sony_return_observed: true,
|
|
raw_rax_u64_hex: '0x0000000000000000',
|
|
raw_low32_hex: '0x00000000',
|
|
raw_signed32: 0,
|
|
timed_out: false,
|
|
tracee_lost: false,
|
|
wrapper_rc: 0,
|
|
wrapper_hex: '0x00000000',
|
|
branch_taken: 'accepted',
|
|
};
|
|
}
|
|
|
|
function observation({ preAppId = 0, appIds = [0, 0, 0, 0, 0, 0, 0], shellUiPids = [58, 58, 58, 58, 58, 58, 58], complete = true } = {}) {
|
|
return {
|
|
schema: SCHEMA,
|
|
diagnostic_identity: IDENTITY,
|
|
base_revision: BASE_REVISION,
|
|
operation_id: OPERATION_ID,
|
|
title_id: TITLE_ID,
|
|
shellui: { pid_before: 58, pid_after: 58, same_process: true, init_rc: 0, ready: true },
|
|
foreground_user_present: true,
|
|
attempts: [attempt()],
|
|
internal_retry_occurred: false,
|
|
retry_delay_ms: 0,
|
|
terminal_attempt_index: 1,
|
|
terminal_wrapper_rc: 0,
|
|
terminal_wrapper_hex: '0x00000000',
|
|
provider_normalized_result: 'accepted',
|
|
provider_accepted: true,
|
|
application_transition: {
|
|
pre_running_big_app_id: preAppId,
|
|
samples: SAMPLE_OFFSETS_MS.map((offset, index) => ({
|
|
scheduled_offset_ms: offset,
|
|
observed_after_ms: offset,
|
|
running_big_app_id: appIds[index],
|
|
shellui_pid: shellUiPids[index],
|
|
process_inventory_complete: complete,
|
|
target_pid: null,
|
|
})),
|
|
},
|
|
};
|
|
}
|
|
|
|
test('accepted LNC without big-app allocation is normalized as the first downstream boundary', () => {
|
|
const result = normalizeShellUiTransitionObservation(observation(), { operationId: OPERATION_ID, titleId: TITLE_ID });
|
|
assert.equal(result.providerAccepted, true);
|
|
assert.equal(result.applicationTransition.classification, 'accepted_lnc_without_big_app_transition');
|
|
assert.equal(result.applicationTransition.bigAppTransitionObserved, false);
|
|
assert.equal(result.applicationTransition.exactTargetProcessObserved, false);
|
|
assert.equal(result.applicationTransition.outcomeQualified, true);
|
|
assert.equal(result.applicationTransition.timingQualified, true);
|
|
assert.deepEqual(result.applicationTransition.sampleOffsetsMs, SAMPLE_OFFSETS_MS);
|
|
});
|
|
|
|
test('Sony no-big-app sentinel -1 is idle while other negative values stay fail-closed', () => {
|
|
const idle = normalizeShellUiTransitionObservation(observation({ preAppId: -1, appIds: [-1, -1, -1, -1, -1, -1, -1] }), { operationId: OPERATION_ID, titleId: TITLE_ID });
|
|
assert.equal(idle.applicationTransition.cleanBaseline, true);
|
|
assert.equal(idle.applicationTransition.bigAppSampleDomainQualified, true);
|
|
assert.equal(idle.applicationTransition.classification, 'accepted_lnc_without_big_app_transition');
|
|
assert.equal(idle.applicationTransition.outcomeQualified, true);
|
|
assert.equal(idle.applicationTransition.baselineIdleSentinel, -1);
|
|
|
|
const ambiguous = normalizeShellUiTransitionObservation(observation({ preAppId: -2, appIds: [-2, -2, -2, -2, -2, -2, -2] }), { operationId: OPERATION_ID, titleId: TITLE_ID });
|
|
assert.equal(ambiguous.applicationTransition.cleanBaseline, false);
|
|
assert.equal(ambiguous.applicationTransition.bigAppSampleDomainQualified, false);
|
|
assert.equal(ambiguous.applicationTransition.classification, 'outcome_unknown');
|
|
});
|
|
|
|
test('new positive running-big-app id is retained but never promoted to exact executable proof', () => {
|
|
const result = normalizeShellUiTransitionObservation(observation({ appIds: [0, 0, 0, 41, 41, 41, 41] }), { operationId: OPERATION_ID, titleId: TITLE_ID });
|
|
assert.equal(result.applicationTransition.classification, 'big_app_transition_observed_unattributed');
|
|
assert.equal(result.applicationTransition.bigAppTransitionObserved, true);
|
|
assert.equal(result.applicationTransition.firstPositiveOffsetMs, 250);
|
|
assert.equal(result.applicationTransition.exactTargetProcessObserved, false);
|
|
assert.equal(result.applicationTransition.launchVerified, false);
|
|
assert.equal(result.applicationTransition.productionRestorable, false);
|
|
});
|
|
|
|
test('pre-existing big app, incomplete inventory, ShellUI drift and hostile sample shapes fail closed', () => {
|
|
for (const input of [
|
|
observation({ preAppId: 7, appIds: [7, 7, 7, 7, 7, 7, 7] }),
|
|
observation({ complete: false }),
|
|
observation({ shellUiPids: [58, 58, 58, 59, 59, 59, 59] }),
|
|
]) {
|
|
const result = normalizeShellUiTransitionObservation(input, { operationId: OPERATION_ID, titleId: TITLE_ID });
|
|
assert.equal(result.applicationTransition.classification, 'outcome_unknown');
|
|
assert.equal(result.applicationTransition.outcomeQualified, false);
|
|
}
|
|
const missing = observation(); missing.application_transition.samples.pop();
|
|
assert.throws(() => normalizeShellUiTransitionObservation(missing, { operationId: OPERATION_ID, titleId: TITLE_ID }), error => error.code === 'PS5_FPKG_TRANSITION_OBSERVATION_INVALID');
|
|
const extra = observation(); extra.application_transition.samples[0].raw_pointer = '0xdeadbeef';
|
|
assert.throws(() => normalizeShellUiTransitionObservation(extra, { operationId: OPERATION_ID, titleId: TITLE_ID }), error => error.code === 'PS5_FPKG_TRANSITION_OBSERVATION_INVALID');
|
|
const slow = observation(); slow.application_transition.samples.at(-1).observed_after_ms = 5001;
|
|
const slowResult = normalizeShellUiTransitionObservation(slow, { operationId: OPERATION_ID, titleId: TITLE_ID });
|
|
assert.equal(slowResult.applicationTransition.classification, 'outcome_unknown');
|
|
assert.equal(slowResult.applicationTransition.timingQualified, false);
|
|
});
|
|
|
|
test('host plan is digest-bound, observation-only and forbids every consumed title', () => {
|
|
const plan = createPs5ShellUiTransitionHostPlan({
|
|
engine: { path: 'build/diagnostics/ps5upload-p15-a10/ps5upload-engine', size: 123, sha256: 'a'.repeat(64) },
|
|
payload: { path: 'build/diagnostics/ps5upload-p15-a10/ps5upload.elf', size: 456, sha256: 'b'.repeat(64) },
|
|
source: { revision: BASE_REVISION, sha256: 'c'.repeat(64), diffSha256: 'd'.repeat(64) },
|
|
previousResult: { titleId: 'CHIM00002', installedObserved: true, launchRequests: 1, clientRetries: 0, terminalCode: 'PS5_FPKG_SHELLUI_LAUNCH_FLOW_FAILED' },
|
|
});
|
|
assert.match(plan.planDigest, /^[a-f0-9]{64}$/);
|
|
assert.equal(plan.liveActionAuthorized, false);
|
|
assert.equal(plan.requiresFreshDistinctTitle, true);
|
|
assert.deepEqual(plan.consumedTitleIds, ['FAKE00000', 'CHIM00001', 'CHIM00002']);
|
|
assert.equal(plan.runtime.identity, IDENTITY);
|
|
assert.deepEqual(plan.runtime.sampleOffsetsMs, SAMPLE_OFFSETS_MS);
|
|
assert.equal(Object.isFrozen(plan), true);
|
|
assert.throws(() => createPs5ShellUiTransitionHostPlan({
|
|
engine: { path: 'x', size: 1, sha256: 'a'.repeat(64) }, payload: { path: 'y', size: 1, sha256: 'b'.repeat(64) },
|
|
source: { revision: BASE_REVISION, sha256: 'c'.repeat(64), diffSha256: 'd'.repeat(64) },
|
|
previousResult: { titleId: 'CHIM00002', installedObserved: false, launchRequests: 1, clientRetries: 0, terminalCode: 'PS5_FPKG_SHELLUI_LAUNCH_FLOW_FAILED' },
|
|
}), error => error.code === 'PS5_FPKG_TRANSITION_PLAN_PRECONDITION');
|
|
});
|
|
|
|
function response(body, status = 200) {
|
|
const serialized = JSON.stringify(body);
|
|
return { ok: status >= 200 && status < 300, status, headers: { get: name => name === 'content-length' ? String(Buffer.byteLength(serialized)) : null }, json: async () => body, text: async () => serialized };
|
|
}
|
|
|
|
test('provider accepts only the exact A10 diagnostic identity and returns correlated transition evidence', async t => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-a10-provider-'));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const pkg = path.join(root, 'control.pkg');
|
|
const bytes = Buffer.concat([Buffer.from([0x7f, 0x46, 0x49, 0x48]), Buffer.alloc(64, 0x41)]);
|
|
fs.writeFileSync(pkg, bytes);
|
|
const component = { type: 'base', componentId: 'base', size: bytes.length, sha256: require('node:crypto').createHash('sha256').update(bytes).digest('hex'), contentId: `UP9000-${TITLE_ID}_00-A10CONTROL000003`, metadata: { absolutePath: pkg, titleId: { value: TITLE_ID } } };
|
|
const provider = createPs5UploadPackageInstallerProvider({
|
|
engineOrigin: 'http://127.0.0.1:19113', consoleHost: 'ps5.local', packageRoot: root,
|
|
launchDiagnostic: P15_SHELLUI_TRANSITION_DIAGNOSTIC,
|
|
getRuntimeIdentity: async () => ({
|
|
schema: 'chimera.ps5upload-runtime-identity.v1',
|
|
expected: { version: '5.4.19', revision: BASE_REVISION, engineSha256: 'e'.repeat(64), payloadSha256: 'f'.repeat(64), dpiSha256: null, diagnostic: { identity: IDENTITY, baseRevision: BASE_REVISION, sourceSha256: '1'.repeat(64), diffSha256: '2'.repeat(64) } },
|
|
engine: { version: '5.4.19', revision: BASE_REVISION, observedSha256: 'e'.repeat(64), evidence: 'test', diagnosticIdentity: IDENTITY },
|
|
payload: { version: '5.4.19', revision: BASE_REVISION, observedSha256: 'f'.repeat(64), evidence: 'test', diagnosticIdentity: IDENTITY },
|
|
dpi: { active: false },
|
|
}),
|
|
fetch: async (url, options = {}) => {
|
|
const value = String(url);
|
|
if (value.endsWith('/api/version')) return response({ version: '5.4.19', diagnostic_identity: IDENTITY, base_revision: BASE_REVISION, observation_schema: SCHEMA });
|
|
if (value.includes('/api/ps5/status')) return response({ version: '5.4.19', ucred_elevated: true });
|
|
if (value.includes('/api/ps5/readiness')) return response({ ready: true });
|
|
if (value.includes('/api/ps5/process/list')) return response({ processes: [{ pid: 7, name: 'kstuff.elf' }, { pid: 8, name: 'etaHEN' }], truncated: false });
|
|
if (value.includes('/api/ps5/syslog/tail')) return response({ text: '' });
|
|
if (value.includes('/api/ps5/app/launch')) {
|
|
const body = JSON.parse(options.body);
|
|
assert.equal(body.operation_id, OPERATION_ID);
|
|
return response({ ok: true, launch_observation: observation() });
|
|
}
|
|
return response({});
|
|
},
|
|
});
|
|
const preflight = await provider.preflightLaunch({ titleId: TITLE_ID, components: [component] });
|
|
assert.equal(preflight.ready, true);
|
|
assert.equal(preflight.runtime.diagnostic.identity, IDENTITY);
|
|
const launched = await provider.launch({ titleId: TITLE_ID, operationId: OPERATION_ID });
|
|
assert.equal(launched.accepted, true);
|
|
assert.equal(launched.diagnostic.applicationTransition.classification, 'accepted_lnc_without_big_app_transition');
|
|
assert.equal(launched.launchVerified, false);
|
|
});
|