'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const crypto = require('crypto'); const fs = require('fs'); const JSZip = require('jszip'); const os = require('os'); const path = require('path'); const { SettingsStore } = require('../../src/persistence/settings-store'); const { createEcosystemService, patchIni, iniDiff, SERVICE_DEFINITIONS, parseRepoArchive, parseItemzflowLog, parsePkgbuild } = require('../../src/services/ecosystem-service'); function fixture() { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ecosystem-')); const paths = Object.fromEntries(['backups', 'cache', 'tmp'].map(name => [name, path.join(root, name)])); for (const directory of Object.values(paths)) fs.mkdirSync(directory, { recursive: true }); const settings = new SettingsStore(path.join(root, 'config', 'settings.json')); settings.set('ftp.host', '127.0.0.1'); const secretValues = new Map(); const secrets = { set(name, value) { if (value) secretValues.set(name, value); else secretValues.delete(name); }, has: name => secretValues.has(name), get: name => secretValues.get(name) || '', }; const remote = new Map([ ['/data/etaHEN/config.ini', Buffer.from('# preserve\nFTP=1\nPS5Debug=0\nUnknownFutureKey=42\n')], ['/data/homebrew/Demo/eboot.elf', Buffer.from('ELF')], ['/data/homebrew/Demo/sce_sys/icon0.png', Buffer.from('PNG')], ['/data/etaHEN/plugins/example.elf', Buffer.from('binary TEST00001 version 1.23')], ['/mnt/usb0/etaHEN/plugins/example.elf', Buffer.from('usb TEST00001 version 1.24')], ]); const directories = new Set([ '/data', '/data/etaHEN', '/data/etaHEN/plugins', '/data/homebrew', '/data/homebrew/Demo', '/data/homebrew/Demo/sce_sys', '/mnt', '/mnt/usb0', '/mnt/usb0/etaHEN', '/mnt/usb0/etaHEN/plugins', ]); const children = directory => { const rows = new Map(); for (const candidate of directories) { if (candidate === directory || path.posix.dirname(candidate) !== directory) continue; rows.set(path.posix.basename(candidate), { name: path.posix.basename(candidate), isDir: true, size: 0 }); } for (const [candidate, content] of remote) { if (path.posix.dirname(candidate) !== directory) continue; rows.set(path.posix.basename(candidate), { name: path.posix.basename(candidate), isDir: false, size: content.length }); } return [...rows.values()]; }; const ftp = { isConnected: () => true, hasCredentials: () => true, listDir: async directory => { if (!directories.has(directory)) throw new Error('missing directory'); return children(directory); }, downloadFile: async (remotePath, localPath) => { if (!remote.has(remotePath)) throw new Error('missing file'); fs.writeFileSync(localPath, remote.get(remotePath)); }, uploadFile: async (localPath, remotePath) => { remote.set(remotePath, fs.readFileSync(localPath)); let current = path.posix.dirname(remotePath); while (current && current !== '.') { directories.add(current); if (current === '/') break; current = path.posix.dirname(current); } }, deleteRemote: async remotePath => remote.delete(remotePath), renameRemote: async (source, destination) => { if (!remote.has(source)) throw new Error('missing source'); remote.set(destination, remote.get(source)); remote.delete(source); }, }; const db = { listGames: () => [{ game_id: 'CUSA00001', title: 'Test Game', availability: { confirmedInstallCount: 1 }, consoleInstalls: [{ installState: 'observed', installedVersion: '1.00' }] }], }; const service = createEcosystemService({ db, ftp, settings, secrets, paths, logger: { info() {}, warn() {}, error() {} } }); return { root, paths, settings, secrets, remote, service }; } test('etaHEN INI patching preserves comments and unknown future keys', () => { const before = '# comment\nFTP=1\nFutureSetting=abc\n'; const after = patchIni(before, { FTP: 0, Klog: 1 }); assert.match(after, /^# comment/m); assert.match(after, /^FutureSetting=abc/m); assert.match(after, /^FTP=0/m); assert.match(after, /^Klog=1/m); assert.deepEqual(iniDiff(before, after).map(item => item.key).sort(), ['FTP', 'Klog']); assert.throws(() => patchIni(before, { UnknownSetting: 1 }), /Onbekende etaHEN-instelling/); }); test('service matrix covers every Chimera PS5 integration on unique ports', () => { const ports = SERVICE_DEFINITIONS.map(service => service.port); assert.equal(new Set(ports).size, ports.length); for (const required of [1337, 2121, 9021, 6969, 8080, 8082, 8084, 9090, 9081, 9112, 3232, 12800]) { assert.ok(ports.includes(required), `missing service port ${required}`); } }); test('GitHub branch archives are parsed with their generated root removed', async () => { const zip = new JSZip(); zip.file('PS5_Cheats-main/json/PPSA00001_1.00.json', '{"mods":[]}'); zip.file('PS5_Cheats-main/README.md', '# Cheats'); const entries = await parseRepoArchive(await zip.generateAsync({ type: 'nodebuffer' })); assert.deepEqual(entries.map(entry => entry.path).sort(), ['README.md', 'json/PPSA00001_1.00.json']); assert.equal(await entries.find(entry => entry.path.endsWith('.json')).readText(), '{"mods":[]}'); await assert.rejects(() => entries.find(entry => entry.path.endsWith('.json')).readText(2), /te groot/); }); test('Itemzflow parser separates jobs, warnings and real errors without false positives', () => { const parsed = parseItemzflowLog([ '[2026-07-16 12:00:00] Installing package 25%', '[2026-07-16 12:00:01] Daemon returned with no errors', '[2026-07-16 12:00:02] WARNING mount retry', '[2026-07-16 12:00:03] ERROR dump failed', ].join('\n'), { id: 'app', label: 'App', path: '/app.log' }); assert.equal(parsed.lines, 4); assert.equal(parsed.errorCount, 1); assert.equal(parsed.warningCount, 1); assert.equal(parsed.latestProgress.progress, 25); assert.ok(parsed.events.some(event => event.type === 'install')); assert.ok(parsed.events.some(event => event.type === 'error')); }); test('PacBrew PKGBUILD metadata is parsed as inert text', () => { const metadata = parsePkgbuild(`pkgname=ps5-payload-websrv\npkgver=git\npkgdesc="A web server"\nlicense=('GPLv3')\ndepends=('ps5-payload-libmicrohttpd')\nmakedepends=('ps5-payload-sdk' 'ps5-payload-libsmb2')\nsource=("git+https://github.com/ps5-payload-dev/websrv.git")\n`); assert.equal(metadata.pkgname, 'ps5-payload-websrv'); assert.equal(metadata.description, 'A web server'); assert.deepEqual(metadata.licenses, ['GPLv3']); assert.deepEqual(metadata.dependencies, ['ps5-payload-libmicrohttpd']); assert.deepEqual(metadata.buildDependencies, ['ps5-payload-sdk', 'ps5-payload-libsmb2']); }); test('Itemzflow log center combines every available official log and tolerates missing logs', async () => { const f = fixture(); try { f.remote.set('/user/app/ITEM00001/logs/itemzflow_app.log', Buffer.from('Installing package 50%\nDone\n')); f.remote.set('/data/itemzflow_daemon/daemon.log', Buffer.from('WARNING NFS mount retry\n')); const packet = await f.service.itemzflowStatus(); assert.equal(packet.online, true); assert.equal(packet.logs.length, 5); assert.equal(packet.health.available, 2); assert.equal(packet.health.missing, 3); assert.equal(packet.health.errors, 0); assert.equal(packet.health.warnings, 1); assert.equal(packet.latestProgress.progress, 50); assert.ok(packet.events.some(event => event.source === 'daemon')); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('etaHEN profile changes are snapshotted, uploaded and verified', async () => { const f = fixture(); try { const current = await f.service.getEtaHenConfig(); const preview = await f.service.previewEtaHen({ raw: current.raw, patch: { PS5Debug: 1, Klog: 1 } }); assert.equal(preview.diff.length, 2); assert.match(preview.proposed, /UnknownFutureKey=42/); const result = await f.service.applyEtaHen({ proposed: preview.proposed, expectedHash: current.hash, label: 'Test' }); assert.equal(result.changed, 2); assert.equal(f.service.etaHistory().length, 1); assert.match(f.remote.get('/data/etaHEN/config.ini').toString(), /PS5Debug=1/); assert.match(f.remote.get('/data/etaHEN/config.ini').toString(), /Klog=1/); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('homebrew and plugins are inventoried with icons, priority and safe toggles', async () => { const f = fixture(); try { const homebrew = await f.service.scanHomebrew(); assert.equal(homebrew.items.length, 1); assert.equal(homebrew.items[0].name, 'Demo'); assert.match(homebrew.items[0].iconUrl, /^\/pork-cache\/homebrew-icons\//); let plugins = await f.service.scanPlugins(); assert.equal(plugins.items.length, 2); assert.equal(plugins.items.filter(item => item.duplicate).length, 2); assert.equal(plugins.items.find(item => item.location === 'usb0').effective, true); assert.equal(plugins.skippedUnmountedUsbRoots, 6); const internal = plugins.items.find(item => item.location === 'internal'); const inspected = await f.service.inspectPlugin({ id: internal.id }); assert.deepEqual(inspected.titleIds, ['TEST00001']); const toggled = await f.service.togglePlugin({ id: internal.id, enabled: false }); assert.equal(toggled.ok, true); assert.equal(f.remote.has('/data/etaHEN/plugins/example.elf.disabled'), true); plugins = toggled.inventory; assert.equal(plugins.items.find(item => item.location === 'internal').enabled, false); const pluginBytes = Buffer.from('plugin TEST00002 version 2.00'); const installed = await f.service.installPlugin({ filename: 'new-plugin.prx', data: pluginBytes, targetRoot: '/data/etaHEN/plugins', }); assert.equal(installed.path, '/data/etaHEN/plugins/new-plugin.prx'); assert.equal(installed.sha256, crypto.createHash('sha256').update(pluginBytes).digest('hex')); assert.deepEqual(f.remote.get(installed.path), pluginBytes); assert.ok(installed.inventory.items.some(item => item.filename === 'new-plugin.prx')); await assert.rejects( () => f.service.installPlugin({ filename: '../unsafe.prx', data: pluginBytes, targetRoot: '/data/etaHEN/plugins' }), /valid \.elf or \.prx plugin file/ ); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('cheat catalog is filtered and paginated server-side', () => { const f = fixture(); try { const items = Array.from({ length: 135 }, (_, index) => ({ key: `cheat-${index}`, title: `Catalog Game ${String(index).padStart(3, '0')}`, gameId: index === 0 ? 'CUSA00001' : `PPSA${String(index).padStart(5, '0')}`, sourceName: 'Test Source', sourceId: 'test', path: `/json/cheat-${index}.json`, platform: index % 2 ? 'ps5' : 'ps4', kind: 'cheat', format: 'json', })); const cacheFile = path.join(f.paths.cache, 'ecosystem', 'cheats-v2.json'); fs.writeFileSync(cacheFile, JSON.stringify({ schema: 1, syncedAt: 123, items, errors: [] })); const first = f.service.cheatIndex({ limit: 50 }); assert.equal(first.total, 135); assert.equal(first.items.length, 50); assert.equal(first.offset, 0); assert.equal(first.nextOffset, 50); assert.equal(first.previousOffset, null); const last = f.service.cheatIndex({ limit: 50, offset: first.nextOffset }); assert.equal(last.items.length, 50); assert.equal(last.previousOffset, 0); assert.equal(last.nextOffset, 100); const installed = f.service.cheatIndex({ installedOnly: true, limit: 50 }); assert.equal(installed.total, 1); assert.equal(installed.items[0].installedTitle, 'Test Game'); const searched = f.service.cheatIndex({ query: 'Catalog Game 134', limit: 50 }); assert.equal(searched.total, 1); assert.equal(searched.items[0].key, 'cheat-134'); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('remote source passwords never leave the encrypted secret interface', () => { const f = fixture(); try { const sources = f.service.saveRemoteSource({ label: 'Unraid', protocol: 'smb', host: 'server.example.test', path: '/Media/roms', username: 'chimera', password: 'secret' }); assert.equal(sources.length, 1); assert.equal(sources[0].passwordConfigured, true); assert.equal(Object.prototype.hasOwnProperty.call(sources[0], 'password'), false); const removed = f.service.removeRemoteSource({ id: sources[0].id }); assert.equal(removed.length, 0); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('PS5 Linux Lab is a read-only preflight and warns about active HEN components', () => { const f = fixture(); try { f.settings.set('operations.compatibility', { firmware: '7.61', hen: 'etaHEN', henVersion: '2.5B', kstuffVersion: '1.07' }); let report = f.service.linuxLab(); assert.equal(report.status, 'compatible'); assert.equal(report.normalizedFirmware, '7.61'); assert.equal(report.policy.payloadSendDisabled, true); assert.equal(report.policy.blockDeviceWritesDisabled, true); assert.equal(report.requirements.find(item => item.id === 'hen').status, 'warning'); assert.equal(report.requirements.find(item => item.id === 'kstuff').status, 'warning'); f.settings.set('operations.compatibility', { firmware: '8.00' }); report = f.service.linuxLab(); assert.equal(report.status, 'incompatible'); assert.equal(report.requirements.find(item => item.id === 'firmware').status, 'incompatible'); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('Save Vault restore verification rejects tampering and accepts only bounded PS5 title archives', async () => { const f = fixture(); try { const titleId = 'PPSA03016'; const id = '1700000000000-test'; const directory = path.join(f.paths.backups, 'save-vault', titleId); fs.mkdirSync(directory, { recursive: true }); const zip = new JSZip(); zip.file(`${titleId}/sdimg_profile`, Buffer.from([2, 4, 6, 8])); zip.file(`${titleId}/sdimg_sce_bu_profile`, Buffer.from([1, 3, 5, 7])); const archive = await zip.generateAsync({ type: 'nodebuffer', compression: 'STORE' }); const filename = `${id}.zip`; fs.writeFileSync(path.join(directory, filename), archive); fs.writeFileSync(path.join(directory, `${id}.json`), JSON.stringify({ schema: 1, id, titleId, createdAt: Date.now(), file: filename, bytes: archive.length, sha256: crypto.createHash('sha256').update(archive).digest('hex'), source: 'test', })); const verified = await f.service.verifySaveSnapshot({ id }); assert.equal(verified.ok, true); assert.equal(verified.archive.titleId, titleId); assert.equal(verified.archive.entries.length, 2); assert.equal(verified.archive.entries.filter(entry => entry.backup).length, 1); fs.appendFileSync(path.join(directory, filename), Buffer.from('tamper')); const rejected = await f.service.verifySaveSnapshot({ id }); assert.equal(rejected.ok, false); assert.match(rejected.error, /SHA-256/); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('Console Timeline persistence is opt-in, bounded, searchable and redacted', () => { const f = fixture(); try { const configured = f.service.configureTimeline({ enabled: true, retentionDays: 7, maxEvents: 100 }); assert.equal(configured.options.enabled, true); f.service.saveRemoteSource({ protocol: 'smb', host: 'nas.local', port: 445, path: '/roms', label: 'password=supersecret' }); const result = f.service.timelineStatus({ search: 'sources', limit: 10 }); assert.equal(result.filtered, 1); assert.doesNotMatch(result.events[0].message, /supersecret/); assert.match(result.events[0].message, /REDACTED/); const exported = f.service.exportTimeline({ limit: 100 }); assert.doesNotMatch(exported, /supersecret/); assert.equal(JSON.parse(exported).redacted, true); f.service.stop(); assert.equal(fs.existsSync(path.join(f.paths.backups, 'ecosystem', 'timeline', 'events.jsonl')), true); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } });