'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); });