210 lines
13 KiB
JavaScript
210 lines
13 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 { createPs5NativePackageInstallService } = require('../../src/services/ps5-native-package-install-service');
|
|
|
|
function fixture(t, overrides = {}) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-p15-a11-evidence-'));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const stateFile = path.join(root, 'operations.json');
|
|
const diagnostic = {
|
|
schema: 'ps5upload.shellui-launch-observation.v1',
|
|
diagnosticIdentity: 'chimera-p15-shellui-transition-observe-v1',
|
|
shellUi: { pidBefore: 110, pidAfter: 110, sameProcess: true, initRc: 0, ready: true },
|
|
attempts: [{ index: 1, callEntered: true, sonyReturnObserved: true, rawSigned32: 0, wrapperRc: 0 }],
|
|
terminalAttemptIndex: 1,
|
|
terminalWrapperRc: 0,
|
|
providerAccepted: true,
|
|
providerNormalizedResult: 'accepted',
|
|
applicationTransition: {
|
|
schema: 'chimera.ps5-shellui-application-transition.v1', preRunningBigAppId: -1,
|
|
samples: [0, 50, 100, 250, 500, 1000, 2000].map(offset => ({ scheduledOffsetMs: offset, observedAfterMs: offset, runningBigAppId: -1, shellUiPid: 110, processInventoryComplete: true, targetPid: null })),
|
|
sampleOffsetsMs: [0, 50, 100, 250, 500, 1000, 2000], cleanBaseline: false, shellUiStable: true,
|
|
inventoryComplete: true, timingQualified: true, bigAppTransitionObserved: false, firstPositiveOffsetMs: null,
|
|
exactTargetProcessObserved: false, outcomeQualified: false, classification: 'outcome_unknown',
|
|
launchVerified: false, productionRestorable: false,
|
|
},
|
|
};
|
|
const operation = {
|
|
id: 'a10-operation-0001', titleId: 'CHIM00003', packageSetId: 10,
|
|
status: 'launch_failed', launchOutcome: 'failed', launchVerified: false,
|
|
productionRestorable: false, installed: true, installedContentVerified: false,
|
|
packageIdentitySha256: '5'.repeat(64), createdAt: 100, updatedAt: 200,
|
|
launchStartedAt: 120, launchFailedAt: 180,
|
|
components: [{ type: 'base', componentId: 'base', sha256: 'a'.repeat(64), size: 68, source: { metadata: { absolutePath: '/private/staging/control.pkg' } } }],
|
|
error: { code: 'PS5_FPKG_SHELLUI_POST_LNC_FAILED', message: 'Failed before stable execution.' },
|
|
launchEvidence: {
|
|
schema: 'chimera.ps5-fpkg-launch-evidence.v2', titleId: 'CHIM00003',
|
|
packageIdentitySha256: '5'.repeat(64), runtimeFingerprint: 'f'.repeat(64),
|
|
runtime: { ready: true, capabilities: { foregroundClean: true } },
|
|
logWatermark: { sha256: '0'.repeat(64), bytes: 0 },
|
|
registration: { required: false, registered: true },
|
|
request: { accepted: true, diagnostic }, requestedAt: 130, preflightCompletedAt: 125,
|
|
firstObservedAt: null, observationCount: 0, process: null,
|
|
rawEvidence: [{
|
|
observedAt: 150,
|
|
summary: {
|
|
observed: false, process: null, failureBoundary: 'ShellUI launch flow',
|
|
failureCode: 'PS5_FPKG_SHELLUI_LAUNCH_FLOW_FAILED', normalizedEvents: [],
|
|
processListTruncated: false,
|
|
syslog: { continuity: 'exact-watermark', currentSha256: 'b'.repeat(64), currentWindowBytes: 29, currentWindowSha256: 'c'.repeat(64) },
|
|
},
|
|
evidence: { truncated: true, sha256: 'd'.repeat(64), preview: '{"currentWindow":"LaunchFlowError.CHIM00003"}' },
|
|
}],
|
|
},
|
|
...overrides,
|
|
};
|
|
fs.writeFileSync(stateFile, JSON.stringify([operation], null, 2));
|
|
let providerCalls = 0;
|
|
const provider = {
|
|
preflightInstall: async () => { providerCalls += 1; return {}; },
|
|
install: async () => { providerCalls += 1; return {}; },
|
|
installStatus: async () => { providerCalls += 1; return {}; },
|
|
preflightLaunch: async () => { providerCalls += 1; return {}; },
|
|
launch: async () => { providerCalls += 1; return {}; },
|
|
observeLaunch: async () => { providerCalls += 1; return {}; },
|
|
};
|
|
const service = createPs5NativePackageInstallService({ provider, stateFile });
|
|
return { root, stateFile, operation, service, providerCalls: () => providerCalls };
|
|
}
|
|
|
|
test('retained A10 evidence export is lossless for the bounded durable record and classifies the post-LNC boundary', t => {
|
|
const f = fixture(t);
|
|
const before = fs.readFileSync(f.stateFile);
|
|
const beforeStat = fs.statSync(f.stateFile);
|
|
const result = f.service.retainedLaunchEvidence('a10-operation-0001');
|
|
|
|
assert.equal(result.schema, 'chimera.ps5-fpkg-retained-launch-export.v1');
|
|
assert.equal(result.operation.titleId, 'CHIM00003');
|
|
assert.equal(result.operation.launchVerified, false);
|
|
assert.equal(result.operation.productionRestorable, false);
|
|
assert.equal(result.analysis.requestAccepted, true);
|
|
assert.equal(result.analysis.terminalSonyResultAccepted, true);
|
|
assert.equal(result.analysis.exactTitleProcessObservationCount, 0);
|
|
assert.equal(result.analysis.stableExactTitleProcess, false);
|
|
assert.equal(result.analysis.firstUnprovenBoundary, 'after-accepted-shellui-lnc-before-big-app-allocation');
|
|
assert.equal(result.analysis.applicationTransition.retainedClassification, 'outcome_unknown');
|
|
assert.equal(result.analysis.applicationTransition.retrospectiveClassification, 'accepted_lnc_without_big_app_transition');
|
|
assert.equal(result.analysis.applicationTransition.classificationChanged, true);
|
|
assert.equal(result.analysis.applicationTransition.baselineIdleSentinel, -1);
|
|
assert.equal(result.analysis.applicationTransition.outcomeQualified, true);
|
|
assert.equal(result.analysis.applicationTransition.launchVerified, false);
|
|
assert.equal(result.analysis.applicationTransition.productionRestorable, false);
|
|
assert.equal(result.analysis.applicationTransition.sourceRecordMutated, false);
|
|
assert.equal(result.retention.rawSourceComplete, false);
|
|
assert.equal(result.retention.losslessForRetainedRecord, true);
|
|
assert.equal(result.retention.observationsRetained, 1);
|
|
assert.deepEqual(result.launchEvidence, f.operation.launchEvidence);
|
|
assert.equal(result.launchEvidence.request.diagnostic.attempts[0].rawSigned32, 0);
|
|
assert.equal(result.launchEvidence.request.diagnostic.attempts[0].wrapperRc, 0);
|
|
assert.equal(result.launchEvidence.rawEvidence[0].evidence.preview, '{"currentWindow":"LaunchFlowError.CHIM00003"}');
|
|
assert.equal(result.integrity.algorithm, 'sha256');
|
|
assert.equal(result.integrity.retainedLaunchEvidenceSha256, crypto.createHash('sha256').update(JSON.stringify(f.operation.launchEvidence)).digest('hex'));
|
|
assert.equal(result.integrity.retainedBytes, Buffer.byteLength(JSON.stringify(f.operation.launchEvidence)));
|
|
assert.equal(JSON.stringify(result).includes('/private/staging/control.pkg'), false);
|
|
assert.equal(f.providerCalls(), 0);
|
|
assert.deepEqual(fs.readFileSync(f.stateFile), before);
|
|
assert.equal(fs.statSync(f.stateFile).mtimeMs, beforeStat.mtimeMs);
|
|
});
|
|
|
|
test('retrospective transition analysis is absent for legacy evidence and explicit for malformed retained evidence', t => {
|
|
const legacy = fixture(t, { launchEvidence: {
|
|
schema: 'chimera.ps5-fpkg-launch-evidence.v2',
|
|
request: { accepted: true, diagnostic: { providerAccepted: true } },
|
|
rawEvidence: [],
|
|
} });
|
|
assert.equal(legacy.service.retainedLaunchEvidence('a10-operation-0001').analysis.applicationTransition, null);
|
|
|
|
const malformed = fixture(t, { launchEvidence: {
|
|
schema: 'chimera.ps5-fpkg-launch-evidence.v2',
|
|
request: { accepted: true, diagnostic: {
|
|
providerAccepted: true,
|
|
applicationTransition: { schema: 'unknown', samples: [] },
|
|
} },
|
|
rawEvidence: [],
|
|
} });
|
|
const result = malformed.service.retainedLaunchEvidence('a10-operation-0001').analysis.applicationTransition;
|
|
assert.equal(result.retrospectiveClassification, 'invalid_retained_transition');
|
|
assert.equal(result.classificationChanged, false);
|
|
assert.equal(result.launchVerified, false);
|
|
assert.equal(result.productionRestorable, false);
|
|
assert.equal(result.sourceRecordMutated, false);
|
|
assert.equal(result.errorCode, 'PS5_FPKG_TRANSITION_OBSERVATION_INVALID');
|
|
|
|
const nonMonotone = fixture(t);
|
|
const persisted = JSON.parse(fs.readFileSync(nonMonotone.stateFile, 'utf8'));
|
|
persisted[0].launchEvidence.request.diagnostic.applicationTransition.samples[0].observedAfterMs = 1000;
|
|
fs.writeFileSync(nonMonotone.stateFile, JSON.stringify(persisted, null, 2));
|
|
const restarted = createPs5NativePackageInstallService({
|
|
stateFile: nonMonotone.stateFile,
|
|
provider: { preflightInstall: async () => ({}), install: async () => ({}), installStatus: async () => ({}) },
|
|
});
|
|
assert.equal(restarted.retainedLaunchEvidence('a10-operation-0001').analysis.applicationTransition.retrospectiveClassification, 'invalid_retained_transition');
|
|
});
|
|
|
|
test('retained evidence export is a deep clone and survives service restart without replay', t => {
|
|
const f = fixture(t);
|
|
const first = f.service.retainedLaunchEvidence('a10-operation-0001');
|
|
first.launchEvidence.request.diagnostic.attempts[0].wrapperRc = 99;
|
|
first.analysis.firstUnprovenBoundary = 'tampered';
|
|
assert.equal(f.service.get('a10-operation-0001').launchEvidence.request.diagnostic.attempts[0].wrapperRc, 0);
|
|
|
|
const restarted = createPs5NativePackageInstallService({
|
|
stateFile: f.stateFile,
|
|
provider: { preflightInstall: async () => ({}), install: async () => ({}), installStatus: async () => ({}) },
|
|
});
|
|
const second = restarted.retainedLaunchEvidence('a10-operation-0001');
|
|
assert.equal(second.launchEvidence.request.diagnostic.attempts[0].wrapperRc, 0);
|
|
assert.equal(second.integrity.retainedLaunchEvidenceSha256, first.integrity.retainedLaunchEvidenceSha256);
|
|
assert.equal(f.providerCalls(), 0);
|
|
});
|
|
|
|
test('retained evidence export redacts secret-bearing legacy fields and discloses the loss', t => {
|
|
const f = fixture(t, { launchEvidence: {
|
|
schema: 'chimera.ps5-fpkg-launch-evidence.v2',
|
|
titleId: 'CHIM00003',
|
|
request: { accepted: false, authorization: 'Bearer super-secret', diagnostic: { token: 'query-secret' } },
|
|
rawEvidence: [{ observedAt: 150, summary: { observed: false }, evidence: { log: 'Authorization: Bearer header-secret https://user:pass@example.invalid/path?token=url-secret' } }],
|
|
} });
|
|
const result = f.service.retainedLaunchEvidence('a10-operation-0001');
|
|
const serialized = JSON.stringify(result);
|
|
assert.doesNotMatch(serialized, /super-secret|query-secret|header-secret|user:pass|url-secret/);
|
|
assert.equal(result.retention.losslessForRetainedRecord, false);
|
|
assert.deepEqual(result.retention.redactedPaths.sort(), ['launchEvidence.rawEvidence.0.evidence.log', 'launchEvidence.request.authorization', 'launchEvidence.request.diagnostic.token']);
|
|
assert.match(result.integrity.retainedLaunchEvidenceSha256, /^[a-f0-9]{64}$/);
|
|
});
|
|
|
|
test('retained evidence export fails typed for unavailable and oversized durable evidence', t => {
|
|
const noEvidence = fixture(t, { status: 'installed_observed', launchEvidence: null });
|
|
assert.throws(() => noEvidence.service.retainedLaunchEvidence('missing'), error => error.code === 'PS5_PKG_INSTALL_NOT_FOUND');
|
|
assert.throws(() => noEvidence.service.retainedLaunchEvidence('a10-operation-0001'), error => error.code === 'PS5_FPKG_LAUNCH_EVIDENCE_UNAVAILABLE');
|
|
|
|
const oversized = fixture(t, { launchEvidence: { schema: 'legacy', rawEvidence: [{ evidence: { preview: 'x'.repeat(600 * 1024) } }] } });
|
|
assert.throws(() => oversized.service.retainedLaunchEvidence('a10-operation-0001'), error => error.code === 'PS5_FPKG_LAUNCH_EVIDENCE_OVERSIZE');
|
|
});
|
|
|
|
test('A11 evidence bridge is read-only, exact-operation scoped and never polls launch status', () => {
|
|
const registry = fs.readFileSync(path.resolve(__dirname, '../../src/server/create-registry.js'), 'utf8');
|
|
const preload = fs.readFileSync(path.resolve(__dirname, '../../preload.js'), 'utf8');
|
|
const main = fs.readFileSync(path.resolve(__dirname, '../../main.js'), 'utf8');
|
|
const web = fs.readFileSync(path.resolve(__dirname, '../../renderer/pork-web-shim.js'), 'utf8');
|
|
const evidenceBridge = fs.readFileSync(path.resolve(__dirname, '../../renderer/pork-evidence-bridge.js'), 'utf8');
|
|
const modal = fs.readFileSync(path.resolve(__dirname, '../../renderer/modules/modal.js'), 'utf8');
|
|
assert.match(registry, /name: 'ps5-native-packages:retained-launch-evidence', classification: 'read-only'/);
|
|
assert.match(registry, /retainedLaunchEvidence\(input\.operationId\)/);
|
|
for (const source of [preload, main, `${web}\n${evidenceBridge}`]) assert.match(source, /ps5-native-packages(?::|%3A)retained-launch-evidence/);
|
|
assert.match(preload, /URL\.revokeObjectURL\(url\)/);
|
|
assert.match(modal, /ps5NativePackageRetainedLaunchEvidence\(ps5A10ControlOperationId\)/);
|
|
assert.match(modal, /window\.pork\.download \|\| window\.xavatarAPI\?\.download/);
|
|
assert.match(modal, /op === 'interrupted' \? 'launchInterrupted'/);
|
|
assert.match(modal, /s\.dataset\.operationId !== id/);
|
|
assert.match(modal, /chimera:language-changed/);
|
|
assert.doesNotMatch(modal, /status\.textContent = ps5A10Text\('evidenceDownloaded'\)/);
|
|
assert.doesNotMatch(modal, /retainedLaunchEvidence[\s\S]{0,200}RestoreLaunchStatus/);
|
|
});
|