'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const JSZip = require('jszip'); const { SettingsStore } = require('../../src/persistence/settings-store'); const { createOperationsService, compareVersions, TASK_DEFINITIONS, parseShadowMountStatus, parseShadowMountModel, analyzeShadowMountConfig, } = require('../../src/services/operations-service'); function fixture() { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-operations-')); const paths = Object.fromEntries(['backups', 'cache', 'tmp', 'logs'].map(name => [name, path.join(root, name)])); for (const directory of Object.values(paths)) fs.mkdirSync(directory, { recursive: true }); const gamesRoot = path.join(root, 'games'); fs.mkdirSync(path.join(gamesRoot, 'CUSA00001 Game'), { recursive: true }); fs.mkdirSync(path.join(gamesRoot, 'PPSA99999 Orphan'), { recursive: true }); fs.writeFileSync(path.join(gamesRoot, 'unfinished.pkg.partial'), 'partial'); const settings = new SettingsStore(path.join(root, 'config', 'settings.json')); settings.set('ftp.host', '127.0.0.1'); const runs = []; let restoredDatabase = null; const databaseBuffer = Buffer.alloc(128); databaseBuffer.write('SQLite format 3\0', 0, 'binary'); const db = { startAutomationRun({ task_id, trigger, started_at = Date.now() }) { runs.unshift({ id: runs.length + 1, task_id, trigger, status: 'running', started_at }); return runs[0].id; }, finishAutomationRun(id, patch) { Object.assign(runs.find(run => run.id === id), patch, { finished_at: Date.now(), duration_ms: 1 }); }, listAutomationRuns(limit = 100) { return runs.slice(0, limit); }, exportDatabaseBuffer: () => Buffer.from(databaseBuffer), restoreDatabaseBuffer(buffer) { restoredDatabase = Buffer.from(buffer); return { ok: true, bytes: buffer.length }; }, listGames: () => [{ game_id: 'CUSA00001', title: 'Known game' }], listBackups: () => [ { id: 1, game_id: 'CUSA00001', backup_path: path.join(gamesRoot, 'missing'), source: 'local' }, { id: 2, game_id: 'CUSA00001', backup_path: path.join(gamesRoot, 'CUSA00001 Game'), source: 'local' }, ], }; const remote = new Map([ ['/data/etaHEN/config.ini', Buffer.from('[etaHEN]\n')], ['/data/shadowmount/config.ini', Buffer.from('[shadowmount]\n')], ['/data/shadowmount/manual.lst', Buffer.from('/data/game/CUSA00001.ffpkg\n/mnt/usb0/PPSA99999.ffpfs\n')], ['/data/shadowmount/manual.status', Buffer.from('installed\tCUSA00001\t/data/game/CUSA00001.ffpkg\tKnown game\ndeleted\tPPSA11111\t/mnt/usb0/PPSA11111.exfat\tDeleted game\nlegacy-status\n')], ['/data/shadowmount/debug.log', Buffer.from('ready\n')], ]); const ftp = { isConnected: () => true, hasCredentials: () => true, listDir: async directory => [...remote.entries()] .filter(([remotePath]) => path.posix.dirname(remotePath) === directory) .map(([remotePath, content]) => ({ name: path.posix.basename(remotePath), isDir: false, size: content.length })), downloadFile: async (remotePath, localPath) => { if (!remote.has(remotePath)) throw new Error('missing'); fs.writeFileSync(localPath, remote.get(remotePath)); }, uploadFile: async (localPath, remotePath) => remote.set(remotePath, fs.readFileSync(localPath)), }; const logger = { file: path.join(paths.logs, 'chimera-server.log'), info() {}, warn() {}, error() {} }; fs.writeFileSync(logger.file, JSON.stringify({ time: new Date().toISOString(), level: 'info', values: ['test'] }) + '\n'); const service = createOperationsService({ db, ftp, settings, paths, logger, localRoots: () => [gamesRoot], version: '1.0.9', taskRunners: { 'library-scan': async () => ({ found: 1 }), 'metadata-refresh': async () => ({ queued: 1 }), 'payload-sync': async () => ({ sources: 1 }), }, }); return { root, paths, gamesRoot, settings, runs, db, ftp, remote, service, restored: () => restoredDatabase }; } test('operations service schedules bounded tasks and records runs', async () => { const f = fixture(); try { const configured = f.service.configureTask({ taskId: 'library-scan', enabled: true, intervalMinutes: 1 }); assert.equal(configured.tasks.find(item => item.id === 'library-scan').intervalMinutes, 5); const result = await f.service.runTask('library-scan', 'test'); assert.deepEqual(result, { found: 1 }); assert.equal(f.runs[0].status, 'completed'); assert.equal(f.runs[0].task_id, 'library-scan'); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('automation includes daily cheat and patch synchronization', () => { const task = TASK_DEFINITIONS.find(item => item.id === 'cheat-sync'); assert.ok(task); assert.equal(task.defaultIntervalMinutes, 1_440); assert.equal(task.defaultEnabled, undefined); }); test('ShadowMount library proposal is read-only and preserves unresolved sources', async () => { const f = fixture(); try { f.settings.set('shadowmount.pathMappings', [{ serverRoot: f.gamesRoot, consoleRoot: '/mnt/usb0/games' }]); const before = f.remote.get('/data/shadowmount/manual.lst').toString(); const proposal = await f.service.shadowMountProposal(); assert.equal(proposal.automaticWrite, false); assert.ok(proposal.proposedManual.includes('/mnt/usb0/games/CUSA00001')); assert.equal(f.remote.get('/data/shadowmount/manual.lst').toString(), before); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('recovery vault snapshots, exports and restores local and fixed remote files', async () => { const f = fixture(); try { const snapshot = await f.service.createSnapshot({ label: 'Known good', includeRemote: true }); assert.match(snapshot.id, /^\d{8}T\d{6}Z-[a-f0-9]{8}$/); assert.equal(snapshot.remote.filter(item => item.included).length, 4); assert.equal(f.service.listSnapshots()[0].label, 'Known good'); const archive = await f.service.exportSnapshot(snapshot.id); assert.equal(Buffer.isBuffer(archive), true); assert.ok(archive.length > 100); f.remote.set('/data/shadowmount/config.ini', Buffer.from('changed')); const restored = await f.service.restoreSnapshot({ id: snapshot.id, restoreRemote: true }); assert.equal(restored.ok, true); assert.equal(f.restored().subarray(0, 16).toString('binary'), 'SQLite format 3\0'); assert.equal(f.remote.get('/data/shadowmount/config.ini').toString(), '[shadowmount]\n'); assert.ok(f.service.listSnapshots().length >= 2, 'restore creates a pre-restore safety point'); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('recovery schema 2 verifies hashes, imports under a new id and never auto-restores', async () => { const f = fixture(); try { const source = await f.service.createSnapshot({ label: 'Portable', includeRemote: true }); assert.equal(source.schema, 2); assert.equal(f.service.verifySnapshot(source.id, { requireHashes: true }).integrity, 'sha256'); const archive = await f.service.exportSnapshot(source.id); const imported = await f.service.importSnapshot(archive); assert.notEqual(imported.id, source.id); assert.equal(imported.importedFromId, source.id); assert.equal(imported.restoreExecuted, false); assert.equal(imported.verification.integrity, 'sha256'); assert.equal(f.restored(), null); const zip = await JSZip.loadAsync(archive); zip.file('settings.json', JSON.stringify({ tampered: true })); const tampered = await zip.generateAsync({ type: 'nodebuffer' }); await assert.rejects(() => f.service.importSnapshot(tampered), /SHA-256|grootte/); const unsafe = new JSZip(); unsafe.file('manifest.json', '{}'); unsafe.file('chimera.db', Buffer.alloc(128)); unsafe.file('settings.json', '{}'); unsafe.file('unexpected.bin', 'x'); const unsafeArchive = await unsafe.generateAsync({ type: 'nodebuffer' }); await assert.rejects(() => f.service.importSnapshot(unsafeArchive), /onbekend of onveilig pad/); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('storage, compatibility and ShadowMount validation are conservative', async () => { const f = fixture(); try { const report = f.service.auditStorage(); assert.equal(report.summary.missingBackups, 1); assert.equal(report.summary.duplicateGames, 1); assert.equal(report.summary.partialFiles, 1); assert.equal(report.summary.orphanGameFolders, 1); let compatibility = f.service.setProfile({ firmware: '7.61', hen: 'etaHEN', kstuffVersion: '1.06' }); assert.equal(compatibility.checks.find(item => item.id === 'firmware').status, 'compatible'); assert.equal(compatibility.checks.find(item => item.id === 'shadowmount-kstuff').status, 'warning'); compatibility = f.service.setProfile({ firmware: '11.00', hen: 'etaHEN', kstuffVersion: '1.07' }); assert.equal(compatibility.checks.find(item => item.id === 'firmware').status, 'warning'); assert.equal(compareVersions('1.07', '1.07'), 0); const shadow = await f.service.shadowMountStatus(); assert.equal(shadow.model.summary.active, 2); assert.equal(shadow.model.summary.installed, 1); assert.equal(shadow.model.summary.pending, 1); assert.equal(shadow.model.summary.deleted, 1); assert.equal(shadow.model.summary.experimental, 1); assert.equal(shadow.model.summary.libraryMatches, 1); assert.equal(shadow.model.unknownStatusLines[0].text, 'legacy-status'); assert.deepEqual(f.service.shadowMountForGame('CUSA00001'), { state: 'installed', sources: 1, experimental: false, formats: ['ffpkg'] }); await assert.rejects(() => f.service.saveShadowMount({ manual: '../unsafe' }), /Ongeldig absoluut pad/); const saved = await f.service.saveShadowMount({ config: '[new]\n', manual: '/data/game/CUSA00001\n' }); assert.equal(saved.ok, true); assert.equal(f.remote.get('/data/shadowmount/config.ini').toString(), '[new]\n'); assert.throws(() => f.service.installPackageUrl({ url: 'http://example.test/file.pkg' }), /HTTPS/); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('ShadowMount parsers preserve unknown status and classify supported image formats', () => { const parsed = parseShadowMountStatus('installed\tCUSA12345\t/mnt/usb0/CUSA12345.ffpkg\tDemo\ninvalid line\n'); assert.equal(parsed.entries.length, 1); assert.equal(parsed.entries[0].titleName, 'Demo'); assert.equal(parsed.unknownLines.length, 1); const model = parseShadowMountModel( '/mnt/usb0/CUSA12345.ffpkg\n/mnt/usb0/PPSA54321.ffpfsc\nrelative/path\n', 'installed\tCUSA12345\t/mnt/usb0/CUSA12345.ffpkg\tDemo\n', [{ game_id: 'CUSA12345', title: 'Library title' }], ); assert.equal(model.summary.total, 3); assert.equal(model.summary.installed, 1); assert.equal(model.summary.pending, 1); assert.equal(model.summary.invalid, 1); assert.equal(model.summary.experimental, 1); assert.equal(model.entries[0].format.support, 'recommended'); assert.equal(model.entries[1].format.support, 'experimental'); const safe = analyzeShadowMountConfig('mount_read_only=ro\nforce_mount=off\nexfat_backend=LVD\nscan_interval_seconds=15\nstability_wait_seconds=10\nlvd_pfs_sector_size=32768\nmd_exfat_sector_size=512\nmd_ufs_sector_size=512\nunknown_future_key=kept\n'); assert.equal(safe.ok, true); assert.equal(safe.requiresRiskConfirmation, false); assert.equal(safe.counts.unknown, 1); const risky = analyzeShadowMountConfig('mount_read_only=0\nforce_mount=1\nimage_rw=CUSA12345.ffpkg\n'); assert.equal(risky.ok, true); assert.equal(risky.requiresRiskConfirmation, true); assert.equal(risky.counts.critical, 3); assert.equal(risky.confirmationPhrase, 'IK BEGRIJP SHADOWMOUNT RISICO'); const invalid = analyzeShadowMountConfig('mount_read_only=banana\nscanpath=../relative\nlvd_ufs_sector_size=3000\n'); assert.equal(invalid.ok, false); assert.equal(invalid.counts.invalid, 3); assert.equal(analyzeShadowMountConfig('kstuff_delay=PPSA12345:3601\n').ok, false); }); test('ShadowMount blocks invalid and unconfirmed critical configuration before any snapshot or upload', async () => { const f = fixture(); try { const before = f.service.listSnapshots().length; await assert.rejects(() => f.service.saveShadowMount({ config: 'mount_read_only=banana\n' }), /faalt de preflight/); await assert.rejects(() => f.service.saveShadowMount({ config: 'mount_read_only=0\n' }), /IK BEGRIJP SHADOWMOUNT RISICO/); assert.equal(f.service.listSnapshots().length, before); const saved = await f.service.saveShadowMount({ config: 'mount_read_only=0\n', confirmation: 'IK BEGRIJP SHADOWMOUNT RISICO' }); assert.equal(saved.preflight.requiresRiskConfirmation, true); assert.equal(f.remote.get('/data/shadowmount/config.ini').toString(), 'mount_read_only=0\n'); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); } }); test('diagnostics include sanitized build provenance', async () => { const f = fixture(); const previous = { id: process.env.CHIMERA_BUILD_ID, revision: process.env.CHIMERA_SOURCE_REVISION, tree: process.env.CHIMERA_SOURCE_TREE, builtAt: process.env.CHIMERA_BUILD_DATE, }; process.env.CHIMERA_BUILD_ID = 'abcdef123456'; process.env.CHIMERA_SOURCE_REVISION = 'abcdef1234567890abcdef1234567890abcdef12'; delete process.env.CHIMERA_SOURCE_TREE; process.env.CHIMERA_BUILD_DATE = '2026-07-15T10:00:00Z'; try { const diagnostics = await f.service.diagnosticsSummary(); assert.deepEqual(diagnostics.chimera.build, { id: 'abcdef123456', revision: 'abcdef1234567890abcdef1234567890abcdef12', tree: null, treeDigest: null, builtAt: '2026-07-15T10:00:00.000Z', }); } finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); if (previous.id === undefined) delete process.env.CHIMERA_BUILD_ID; else process.env.CHIMERA_BUILD_ID = previous.id; if (previous.revision === undefined) delete process.env.CHIMERA_SOURCE_REVISION; else process.env.CHIMERA_SOURCE_REVISION = previous.revision; if (previous.tree === undefined) delete process.env.CHIMERA_SOURCE_TREE; else process.env.CHIMERA_SOURCE_TREE = previous.tree; if (previous.builtAt === undefined) delete process.env.CHIMERA_BUILD_DATE; else process.env.CHIMERA_BUILD_DATE = previous.builtAt; } });