Files
chimera-Public/test/server/ch-0406-ps5-content-adapters.test.js

88 lines
5.3 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const {
SHADOWMOUNT_PLUS_PROFILE,
createPs5ContentAdapterService,
createPs5ContentSimulator,
unavailableLiveAdapter,
} = require('../../src/services/ps5-content-adapter-service');
const DIGEST = 'ab'.repeat(32);
const artifact = kind => ({ kind, sourceRef: `library:item-${kind}`, fileName: kind === 'image' ? 'PPSA12345.ffpkg' : 'PPSA12345', sha256: DIGEST, size: 4096 });
const volumeResolver = { resolve: async id => ({ id, adapterId: 'ShadowMountPlus', label: 'USB games', consoleRoots: ['/mnt/usb3'] }) };
test('simulator preserves transfer, stage, mount, register and verification as distinct states', async () => {
const simulator = createPs5ContentSimulator();
const service = createPs5ContentAdapterService({ adapter: simulator.adapter, volumeResolver, uuid: () => 'run-image', now: (() => { let n = 0; return () => ++n; })() });
const result = await service.start({ titleId: 'PPSA12345', volumeId: 'usb-games', artifact: artifact('image') });
assert.equal(result.state, 'verified');
assert.deepEqual(result.history.map(item => item.state), ['planned', 'transferring', 'transferred', 'staging', 'staged', 'mounting', 'mounted', 'registering', 'registered', 'verifying', 'verified']);
assert.deepEqual(simulator.calls, ['transfer', 'stage', 'mount', 'register', 'postMountVerify']);
assert.equal(result.paths.transferPath, '/mnt/usb3/.chimera-transfer/PPSA12345/PPSA12345.ffpkg');
assert.equal(result.paths.stagedPath, '/mnt/usb3/etaHEN/games/PPSA12345/PPSA12345.ffpkg');
assert.equal(result.paths.mountPath, '/mnt/shadowmnt/PPSA12345_simulated');
assert.equal('installed' in result, false);
});
test('folder simulator uses the selected semantic volume and still requires post-mount verification', async () => {
const simulator = createPs5ContentSimulator();
const result = await createPs5ContentAdapterService({ adapter: simulator.adapter, volumeResolver }).start({ titleId: 'PPSA54321', volumeId: 'portable-library', artifact: artifact('folder') });
assert.equal(result.state, 'verified');
assert.match(result.paths.stagedPath, /^\/mnt\/usb3\/etaHEN\/games\/PPSA54321\//);
assert.equal(result.paths.mountPath, result.paths.stagedPath);
assert.ok(result.verificationEvidence);
});
test('unavailable, unpinned and incomplete capability providers fail closed before mutation', async () => {
const unavailable = createPs5ContentAdapterService({ adapter: unavailableLiveAdapter(), volumeResolver });
assert.equal(unavailable.probe().compatible, false);
await assert.rejects(unavailable.start({ titleId: 'PPSA12345', volumeId: 'usb-games', artifact: artifact('image') }), /unavailable or capability mismatch/i);
for (const probe of [
{ version: '1.6test12' },
{ revision: '00'.repeat(20) },
{ capabilities: { semanticVolumePaths: true, transfer: true } },
]) {
const simulator = createPs5ContentSimulator({ probe });
const service = createPs5ContentAdapterService({ adapter: simulator.adapter, volumeResolver });
assert.equal(service.probe().compatible, false);
await assert.rejects(service.start({ titleId: 'PPSA12345', volumeId: 'usb-games', artifact: artifact('image') }), new RegExp(SHADOWMOUNT_PLUS_PROFILE.id));
assert.deepEqual(simulator.calls, []);
}
});
test('adapter-derived paths cannot escape the selected volume or use traversal', async () => {
for (const derived of [
{ transferPath: '/mnt/usb3/../system/PPSA12345.ffpkg', stagedPath: '/mnt/usb3/games/PPSA12345.ffpkg' },
{ transferPath: '/mnt/usb3/incoming/PPSA12345.ffpkg', stagedPath: '/data/etaHEN/games/PPSA12345.ffpkg' },
{ transferPath: 'relative/PPSA12345.ffpkg', stagedPath: '/mnt/usb3/games/PPSA12345.ffpkg' },
]) {
const simulator = createPs5ContentSimulator({ overrides: { derivePaths: () => derived } });
await assert.rejects(
createPs5ContentAdapterService({ adapter: simulator.adapter, volumeResolver }).start({ titleId: 'PPSA12345', volumeId: 'usb-games', artifact: artifact('image') }),
/invalid|unsafe|outside/i,
);
assert.deepEqual(simulator.calls, []);
}
});
test('a timed-out adapter step stops before mount, register and verification', async () => {
const simulator = createPs5ContentSimulator({ delayStep: 'stage', delayMs: 30 });
const service = createPs5ContentAdapterService({ adapter: simulator.adapter, volumeResolver, timeoutMs: 5, uuid: () => 'run-timeout' });
const result = await service.start({ titleId: 'PPSA12345', volumeId: 'usb-games', artifact: artifact('image') });
assert.equal(result.state, 'needs_attention');
assert.equal(result.failedStep, 'stage');
assert.equal(result.error, 'timeout:stage');
assert.deepEqual(simulator.calls, ['transfer', 'stage']);
});
test('registration handoff alone never passes without exact post-mount evidence', async () => {
const simulator = createPs5ContentSimulator({ overrides: { verifyPostMount: async input => ({ mounted: true, registered: false, titleId: input.titleId, mountPath: input.mountPath }) } });
const result = await createPs5ContentAdapterService({ adapter: simulator.adapter, volumeResolver }).start({ titleId: 'PPSA12345', volumeId: 'usb-games', artifact: artifact('image') });
assert.equal(result.state, 'needs_attention');
assert.equal(result.failedStep, 'verifying');
assert.match(result.error, /did not prove/);
});