Files
chimera-Public/test/server/ch-0910-live-trophy-catalog.test.js
T

182 lines
12 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 { createLiveTrophyCatalogService, normalizeCatalog,
normalizeInstalledCatalog: normalizeInstalledServiceCatalog } = require('../../src/services/live-trophy-catalog-service');
const { normalizeActiveCatalog, activeCatalogMarkup, normalizeInstalledCatalog,
installedCatalogMarkup, normalizeTrophyLibrary, trophyLibraryMarkup } = require('../../src/renderer/trophies');
function adapterResult() {
return { status: 'ok', mappingRemoved: true, acquisition: { readOnly: true, persisted: false },
entry: { game: { title: 'Example', trophyCount: 2 }, trophies: [
{ id: 0, grade: 'bronze', hidden: false, name: 'Start', description: 'Begin', observedUnlocked: true },
{ id: 1, grade: 'gold', hidden: true, name: 'Finish', description: 'End', observedUnlocked: false },
] } };
}
test('normalizes only the bounded UI-safe active catalog', () => {
const value = normalizeCatalog(adapterResult());
assert.deepEqual(value.counts, { total: 2, unlocked: 1, locked: 1 });
assert.equal(value.trophies[1].name, 'Finish'); assert.equal(value.trophies[0].unlocked, true);
assert.equal(value.entry, undefined); assert.equal(value.catalogKey, undefined);
assert.deepEqual(value.platform, { family: 'legacy', runsOn: 'ps5', nativePs5: false });
assert.deepEqual(value.capabilities, { liveInventory: true, exactOneUnlock: false });
});
test('hash-bound service invokes one fixed adapter and returns a read-only catalog', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-live-trophy-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'host'), { recursive: true }); fs.mkdirSync(path.join(root, 'artifacts'));
fs.writeFileSync(path.join(root, 'host', 'live-active-catalog-cli.mjs'), 'fixture');
const elf = Buffer.from('resident'); fs.writeFileSync(path.join(root, 'artifacts', 'chimera-ps5-legacy-query-resident.elf'), elf);
const calls = [];
const service = createLiveTrophyCatalogService({ appRoot: root,
settings: { get: key => key === 'ftp.host' ? 'console.test' : '' },
secrets: { get: key => key === 'trophy.releaseKey' ? '00112233445566778899aabbccddeeff' : '' },
adapterRoot: root, expectedElfSha256: crypto.createHash('sha256').update(elf).digest('hex'),
runAdapter: async input => { calls.push(input); return JSON.stringify(adapterResult()); } });
assert.equal(service.status().available, true);
const catalog = await service.activeCatalog();
assert.equal(catalog.readOnly, true); assert.equal(catalog.persisted, false); assert.equal(calls.length, 1);
assert.equal(catalog.platform.nativePs5, false); assert.equal(catalog.capabilities.exactOneUnlock, false);
assert.equal(calls[0].host, 'console.test'); assert.equal(calls[0].releaseKey.length, 32);
});
test('active catalog retries one transient base-map failure and no other failure', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-live-trophy-retry-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'host'), { recursive: true }); fs.mkdirSync(path.join(root, 'artifacts'));
fs.writeFileSync(path.join(root, 'host', 'live-active-catalog-cli.mjs'), 'fixture');
const elf = Buffer.from('resident'); fs.writeFileSync(path.join(root, 'artifacts', 'chimera-ps5-legacy-query-resident.elf'), elf);
const options = { appRoot: root, adapterRoot: root,
settings: { get: () => 'console.test' }, secrets: { get: () => '00112233445566778899aabbccddeeff' },
expectedElfSha256: crypto.createHash('sha256').update(elf).digest('hex') };
let attempts = 0;
const recovered = createLiveTrophyCatalogService({ ...options, runAdapter: async () => {
attempts += 1;
if (attempts === 1) throw new Error('Live trophy adapter failed: LEGACY_QUERY_LOAD_PS5DEBUG_LOAD_BASE_MAP_FAILED');
return JSON.stringify(adapterResult());
} });
assert.equal((await recovered.activeCatalog()).available, true); assert.equal(attempts, 2);
attempts = 0;
const refused = createLiveTrophyCatalogService({ ...options, runAdapter: async () => {
attempts += 1; throw new Error('Live trophy adapter failed: LEGACY_QUERY_GAME_MISSING');
} });
await assert.rejects(refused.activeCatalog(), /LEGACY_QUERY_GAME_MISSING/); assert.equal(attempts, 1);
});
test('production binding is read-only and exposes no arbitrary command or trophy mutation route', () => {
const service = fs.readFileSync(path.join(__dirname, '../../src/services/live-trophy-catalog-service.js'), 'utf8');
const registry = fs.readFileSync(path.join(__dirname, '../../src/server/register-functional-modules.js'), 'utf8');
assert.match(registry, /name: 'trophy:active-catalog'.+classification: 'read-only'/);
assert.match(registry, /name: 'trophy:installed-catalog'.+classification: 'read-only'/);
assert.match(registry, /name: 'trophy:unlock-one'.+classification: 'sensitive'/);
assert.doesNotMatch(service, /shell:\s*true|\bexec\(|upload|writeFile|unlockAll|syncTroph/i);
assert.match(service, /FTP_PORT=1337/); assert.match(service, /DEBUG_PORT=764/);
const web = fs.readFileSync(path.join(__dirname, '../../renderer/pork-web-shim.js'), 'utf8');
for (const method of ['trophyActiveCatalog', 'trophyInstalledCatalog', 'trophyImportedCatalog',
'trophyImportDefinition', 'trophyUnlockOne', 'trophyHistoryImportStatus', 'trophyHistoryPreview',
'trophyHistoryApply']) assert.match(web, new RegExp(method));
});
test('active catalog renderer shows state and conceals locked hidden definitions', () => {
const catalog = normalizeActiveCatalog({ ...normalizeCatalog(adapterResult()),
capabilities: { liveInventory: true, exactOneUnlock: true } });
const markup = activeCatalogMarkup(catalog, (_key, fallback) => fallback);
assert.match(markup, /Example/); assert.match(markup, /1\/2/);
assert.match(markup, /Legacy \/ PS4 trophy API running on PS5/);
assert.match(markup, /Start/); assert.match(markup, /Locked/);
assert.match(markup, /Hidden trophy/); assert.doesNotMatch(markup, /Finish/);
assert.doesNotMatch(markup, /End|data-unlock-id="1"/); assert.doesNotMatch(markup, /name="trophyId"/);
const revealed = activeCatalogMarkup(catalog, (_key, fallback) => fallback, true);
assert.match(revealed, /Finish/); assert.match(revealed, /End/);
assert.match(revealed, /data-unlock-id="1"/); assert.doesNotMatch(revealed, /name="trophyId"/);
});
test('premium trophy library merges game definitions with exact live progress', () => {
const active = normalizeActiveCatalog({ ...normalizeCatalog(adapterResult()),
capabilities: { liveInventory: true, exactOneUnlock: true } });
const installed = normalizeInstalledCatalog({ available: true, readOnly: true, count: 2,
games: [
{ game: { title: 'Example', trophyCount: 2 }, trophies: adapterResult().entry.trophies.map(({ observedUnlocked, ...item }) => item) },
{ game: { title: 'Library Game', trophyCount: 1 }, trophies: [{ id: 0, grade: 'silver', hidden: false, name: 'First step', description: 'Begin' }] },
] });
const library = normalizeTrophyLibrary({ activeCatalog: active, installedCatalog: installed });
assert.equal(library.games.length, 2);
assert.equal(library.games[0].title, 'Example');
assert.equal(library.games[0].coverage, 'live');
assert.deepEqual(library.games[0].counts, { total: 2, earned: 1, locked: 1 });
assert.equal(library.games[0].trophies[1].earned, false);
assert.equal(library.games[0].trophies[1].unlockEligible, true);
assert.equal(library.games[1].coverage, 'definitions');
assert.equal(library.games[1].trophies[0].earned, null);
});
test('premium trophy library renders game cards, filters and catalog-derived unlock actions', () => {
const active = normalizeActiveCatalog({ ...normalizeCatalog(adapterResult()),
capabilities: { liveInventory: true, exactOneUnlock: true } });
const installed = normalizeInstalledCatalog({ available: true, readOnly: true, count: 1,
games: [{ game: { title: 'Example', trophyCount: 2 }, trophies: adapterResult().entry.trophies.map(({ observedUnlocked, ...item }) => item) }] });
const library = normalizeTrophyLibrary({ activeCatalog: active, installedCatalog: installed });
const markup = trophyLibraryMarkup(library, { selectedGameKey: library.games[0].key, revealHidden: true }, (_key, fallback) => fallback);
assert.match(markup, /trophy-library-grid/);
assert.match(markup, /trophy-library-game-card/);
assert.match(markup, /1 of 2 earned/);
assert.match(markup, /data-library-status="locked"/);
assert.match(markup, /data-library-grade="gold"/);
assert.match(markup, /data-library-unlock-id="1"/);
assert.doesNotMatch(markup, /name="trophyId"|unlockAll/);
const stylesheet = fs.readFileSync(path.join(__dirname, '../../renderer/styles/trophies-library.css'), 'utf8');
assert.match(stylesheet, /\.trophy-library-shell/);
assert.match(stylesheet, /scroll-snap-type:x mandatory/);
assert.match(stylesheet, /prefers-reduced-motion:reduce/);
});
test('normalizes a bounded installed definition catalog without identifiers', () => {
const value = normalizeInstalledServiceCatalog({ status: 'ok', readOnly: true, persisted: false,
identifiersReturned: false, count: 1, entries: [{ game: { title: 'Example', trophyCount: 2 },
trophies: adapterResult().entry.trophies.map(({ observedUnlocked, ...item }) => item) }] });
assert.equal(value.count, 1); assert.equal(value.games[0].game.title, 'Example');
assert.equal(JSON.stringify(value).includes('catalogKey'), false);
});
test('installed catalog renderer exposes every game and conceals hidden definitions', () => {
const catalog = normalizeInstalledCatalog({ available: true, readOnly: true, count: 1,
games: [{ game: { title: 'Library Game', trophyCount: 1 }, trophies: [
{ id: 0, grade: 'gold', hidden: true, name: 'Secret', description: 'Spoiler' }
] }] });
const markup = installedCatalogMarkup(catalog, (_key, fallback) => fallback);
assert.match(markup, /Library Game/); assert.match(markup, /Hidden trophy/);
assert.doesNotMatch(markup, /Secret|Spoiler/);
const revealed = installedCatalogMarkup(catalog, (_key, fallback) => fallback, true);
assert.match(revealed, /Secret/); assert.match(revealed, /Spoiler/);
});
test('single unlock requires exact confirmation and independently verifies the result', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-unlock-trophy-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'host'), { recursive: true }); fs.mkdirSync(path.join(root, 'artifacts'));
const activeCli = Buffer.from('active'), unlockCli = Buffer.from('unlock'), activeElf = Buffer.from('reader'), unlockElf = Buffer.from('writer');
fs.writeFileSync(path.join(root, 'host', 'live-active-catalog-cli.mjs'), activeCli);
fs.writeFileSync(path.join(root, 'host', 'legacy-unlock-ps5debug-cli.mjs'), unlockCli);
fs.writeFileSync(path.join(root, 'artifacts', 'chimera-ps5-legacy-query-resident.elf'), activeElf);
fs.writeFileSync(path.join(root, 'artifacts', 'chimera-ps5-legacy-unlock-resident.elf'), unlockElf);
let unlocked = false; const calls = [];
const service = createLiveTrophyCatalogService({ appRoot: root, adapterRoot: root,
settings: { get: () => 'console.test' }, secrets: { get: () => '00112233445566778899aabbccddeeff' },
expectedElfSha256: crypto.createHash('sha256').update(activeElf).digest('hex'),
expectedUnlockElfSha256: crypto.createHash('sha256').update(unlockElf).digest('hex'),
expectedUnlockCliSha256: crypto.createHash('sha256').update(unlockCli).digest('hex'),
runAdapter: async input => { calls.push(input); if (input.consent === 'CONSENT_LEGACY_UNLOCK_ONE_EXECUTE') { unlocked = true; return JSON.stringify({ status: 'ok', trophyId: 1, unlockedAfter: true, mappingRemoved: true, ownedHandleDestroyed: true, psnContacted: false }); }
const value = adapterResult(); if (unlocked) value.entry.trophies[1].observedUnlocked = true; return JSON.stringify(value); } });
await assert.rejects(service.unlockOne({ trophyId: 1, confirmation: 'wrong' }), /Exact confirmation/);
const result = await service.unlockOne({ trophyId: 1, confirmation: 'UNLOCK 1 Example' });
assert.equal(result.status, 'completed'); assert.equal(result.counts.unlocked, 2);
assert.equal(calls.filter(call => call.consent === 'CONSENT_LEGACY_UNLOCK_ONE_EXECUTE').length, 1);
});