'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const modulePathCache = new Map(); let activeTransferState = null; function mockModule(resolvedPath, exportsValue) { const current = require.cache[resolvedPath]; modulePathCache.set(resolvedPath, current); require.cache[resolvedPath] = { id: resolvedPath, filename: resolvedPath, loaded: true, exports: exportsValue, }; } function restoreModules() { for (const [resolvedPath, original] of modulePathCache) { if (original === undefined) delete require.cache[resolvedPath]; else require.cache[resolvedPath] = original; } modulePathCache.clear(); } function installRegistryMocks(rootDir) { const createOperationResult = () => ({ status: 'ok', services: [], metadata: {}, scans: [], health: {}, }); mockModule(path.resolve(rootDir, 'src/platform/data-paths.js'), { mountedRoots: () => [], normalizeAllowedRoots: (roots, fallback) => (Array.isArray(roots) && roots.length ? roots : [fallback]), resolveAllowedPath: (candidate, _roots, options = {}) => { if (!candidate) return options.fallbackRoot || ''; return path.isAbsolute(candidate) ? candidate : path.join(options.fallbackRoot || '', candidate); }, }); mockModule(path.resolve(rootDir, 'src/hashing.js'), { hashFile: async () => Buffer.from('fake-hash'), }); mockModule(path.resolve(rootDir, 'src/services/elf-arsenal-client.js'), { createElfArsenalClient: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/local-library-scanner.js'), { scanLocalLibraries: async () => ({ games: [], added: 0, total: 0 }), }); mockModule(path.resolve(rootDir, 'src/services/transfer-service.js'), { createTransferService: ({ broadcast, getCredentials }) => { const executors = new Map(); const state = { jobs: [], jobsById: new Map(), registerExecutor: (kind, factory) => { executors.set(kind, factory); }, enqueue: (type, options = {}, runner) => { const jobId = `job-${state.jobs.length + 1}`; let status = 'queued'; const progress = {}; const throwIfCancelled = () => { if (status === 'cancelled') { throw new Error('Taak geannuleerd'); } }; const context = { progress: () => {}, throwIfCancelled, client: { uploadFile: async () => ({ success: true }), uploadDirectory: async () => ({ success: true }), downloadFile: async () => ({ success: true }), }, }; const job = { jobId, id: jobId, type, progress, status, promise: Promise.resolve(), deduplicated: false, }; const execute = async () => { status = 'active'; if (runner) { await runner(context); } else { const executor = executors.get(String((options.descriptor || {}).kind || type)); if (!executor) throw new Error(`No executor for ${options.descriptor?.kind || type}`); await executor(options.descriptor || {})(context); } return { success: true, remotePath: options.remotePath }; }; const credentials = getCredentials?.(); if (credentials) { context.credentials = credentials; } const resolved = execute() .then(result => { status = 'done'; broadcast?.('transfer:update', state.state()); return result; }) .catch(error => { status = 'failed'; throw error; }); job.promise = resolved; job.status = status; state.jobs.push(job); state.jobsById.set(jobId, job); broadcast?.('transfer:update', state.state()); return job; }, pause: () => {}, resume: () => {}, clearDone: () => { state.jobs = []; state.jobsById = new Map(); }, setConcurrent: () => 4, setPolicy: options => options, retry: () => false, cancel: () => false, state: () => ({ jobs: state.jobs.slice(), policy: { bandwidthLimitSupported: false } }), close: () => {}, }; activeTransferState = state; return state; }, }); mockModule(path.resolve(rootDir, 'src/services/conversion-service.js'), { createConversionService: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/operations-service.js'), { createOperationsService: () => new Proxy({}, { get: (_target, prop) => { if (prop === 'start' || prop === 'stop') return () => {}; return () => createOperationResult(); }, }), }); mockModule(path.resolve(rootDir, 'src/services/ecosystem-service.js'), { createEcosystemService: () => ({ stop: () => {}, payloads: {}, listRemoteSources: () => [], classification: {}, downloads: () => [], }), }); mockModule(path.resolve(rootDir, 'src/services/ecosystem-intelligence-service.js'), { createEcosystemIntelligenceService: () => ({ latestHealth: () => ({}) }), }); mockModule(path.resolve(rootDir, 'src/services/game-update-service.js'), { createGameUpdateService: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/ps5debug-readonly-client.js'), { createPs5DebugReadonlyClient: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/remote-play-service.js'), { createRemotePlayService: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/backup-replication-service.js'), { createBackupReplicationService: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/payload-runtime-service.js'), { createPayloadRuntimeService: () => ({ watchdog: () => ({}), updatePlan: () => ({}), refreshRepositories: () => ({}), installUpdate: () => ({}), }), }); mockModule(path.resolve(rootDir, 'src/services/chimera-beacon-client.js'), { createChimeraBeaconClient: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/console-profile-service.js'), { createConsoleProfileService: () => { const secrets = new Map(); return { activeId: () => 'console-1', getSecret: key => secrets.get(String(key)), setSecret: (key, value) => { secrets.set(String(key), value); }, syncActiveFromLegacy: () => {}, list: () => [], comparison: () => ({ local: [], remote: [] }), save: () => ({}), activate: () => ({}), remove: () => ({}), storageTopology: () => ({ roots: [], games: [] }), }; }, }); mockModule(path.resolve(rootDir, 'src/services/companion-worker-service.js'), { createCompanionWorkerService: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/audit-log-service.js'), { createAuditLogService: () => ({ query: () => [], health: () => ({ ok: true }), exportAudit: () => Buffer.from('[]'), }), normalizeEntry: () => ({}), LEVELS: {}, CLASSIFICATIONS: {}, }); mockModule(path.resolve(rootDir, 'src/services/ps5-klog-service.js'), { createPs5KlogService: () => ({ start: () => {}, stop: () => {} }), }); mockModule(path.resolve(rootDir, 'src/security/totp.js'), { createTotpService: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/notification-service.js'), { createNotificationService: () => ({ emit: async () => ({}) }), }); mockModule(path.resolve(rootDir, 'src/services/source-classification-service.js'), { createSourceClassificationService: () => ({}), }); mockModule(path.resolve(rootDir, 'src/services/game-metadata-view.js'), { sanitizeOverride: value => value, annotateRegionalVariants: value => value, }); mockModule(path.resolve(rootDir, 'src/security/read-only-sql.js'), { assertReadOnlySql: value => value, }); mockModule(path.resolve(rootDir, 'src/server/register-functional-modules.js'), { registerFunctionalModules: () => ({ metadata: { queue: () => {}, queueDue: () => [], status: () => ({}), sync: () => [], snapshot: () => ({}), }, payloadCatalog: { sync: async () => ({ count: 0 }), seed: () => ({ count: 0 }), snapshot: () => ({}), }, payloadManager: {}, cheatsSync: async () => ({}), }), }); mockModule(path.resolve(rootDir, 'src/services/ezremote-server-client.js'), { createEzRemoteServerClient: () => ({ probe: () => ({ status: 'mock' }), downloads: () => [], waitFor: () => ({}), ensureDownload: async () => ({ completed: true, download: { transferred: 123, size: 123, path: '' } }), }), buildDownloadRequest: input => input, DEFAULT_DESTINATION_ROOTS: ['/games/ps4', '/games/ps5'], }); mockModule(path.resolve(rootDir, 'src/services/console-scan-reconciler.js'), { reconcileConsoleScan: () => ({ applied: true, upserted: 0, cleared: 0 }), }); } function withMockedRegistry(callback) { const rootDir = path.resolve(__dirname, '../..'); let originalCreateRegistry; const registryPath = path.resolve(rootDir, 'src/server/create-registry.js'); let active = true; const cleanup = () => { if (!active) return; active = false; restoreModules(); delete require.cache[registryPath]; }; try { installRegistryMocks(rootDir); delete require.cache[registryPath]; originalCreateRegistry = require(registryPath); const result = callback(originalCreateRegistry.createRegistry); if (result && typeof result.then === 'function') { return result.finally(cleanup); } cleanup(); return result; } finally { if (active === false) return; cleanup(); } } function createDbWithTracking() { const updates = []; const games = new Map(); const db = new Proxy({ __updates: updates, updateGame: (gameId, fields) => { updates.push({ gameId, fields }); const current = games.get(String(gameId)) || { game_id: String(gameId), ftp_path: '' }; games.set(String(gameId), { ...current, ...fields }); }, getGame: gameId => games.get(String(gameId)) || null, upsertGame: game => { if (game?.game_id) games.set(String(game.game_id), { ...(games.get(String(game.game_id)) || {}), ...game }); }, setGamePorked: () => {}, listBackporkFolders: () => [], listBackups: () => [], upsertGameMinimal: () => {}, setGameFirmwareLabel: () => {}, upsertBackup: () => {}, pruneGameFirmwareForFolder: () => {}, pruneOrphanScanGames: () => {}, syncBackedUpFlags: () => {}, beginBulkUpdate: () => {}, endBulkUpdate: () => {}, }, { get(target, property) { return Reflect.get(target, property) || (() => {}); }, }); return { db, updates }; } function waitForJob(transferState, jobId) { const job = transferState.jobsById.get(jobId); if (!job) throw new Error(`Missing job ${jobId}`); return job.promise; } test('FTP upload queue completion does not set installed', async () => { await withMockedRegistry(async (createRegistry) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ch0003-upload-')); try { const localFile = path.join(root, 'game.pkg'); fs.writeFileSync(localFile, 'payload'); const { db, updates } = createDbWithTracking(); db.upsertGame({ game_id: 'PPSA12345', ftp_path: '/existing/path', installed: 1 }); const settings = { get: () => '127.0.0.1', set: () => {} }; const secrets = { get: () => '', has: () => false }; const ftp = { isConnected: () => true, hasCredentials: () => true }; const paths = { root, cache: root, backups: root, tmp: root, config: root }; const logger = { info() {}, warn() {}, error() {}, audit() {} }; const registry = createRegistry({ db, ftp, settings, secrets, paths, logger, capabilities: { features: {} }, broadcast: () => {}, appRoot: root, }); const upload = registry.commands.get('ftp:upload'); assert.ok(upload); const result = upload.handler({ localPath: localFile, remotePath: '/mnt/sandbox' }); await waitForJob(activeTransferState, result.jobId); assert.equal(result.success, true); assert.equal(updates.length, 0, 'no db.updateGame calls expected after upload completion'); } finally { fs.rmSync(root, { recursive: true, force: true }); activeTransferState = null; } }); }); test('Backpork queue completion writes ftp_path but never flips installed', async () => { await withMockedRegistry(async (createRegistry) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ch0003-pork-')); try { const backporkRoot = path.join(root, 'backporks'); fs.mkdirSync(backporkRoot, { recursive: true }); const gameFolder = path.join(backporkRoot, 'PPSA54321-pack'); fs.mkdirSync(gameFolder, { recursive: true }); const { db, updates } = createDbWithTracking(); db.upsertGame({ game_id: 'PPSA54321', ftp_path: '/mnt/sandbox/pfsmnt/PPSA54321', installed: 1 }); const settings = { get: () => '127.0.0.1', set: () => {} }; const secrets = { get: () => '', has: () => false }; const ftp = { isConnected: () => true, hasCredentials: () => true }; const paths = { root, cache: root, backups: root, tmp: root, config: root }; const logger = { info() {}, warn() {}, error() {}, audit() {} }; const registry = createRegistry({ db, ftp, settings, secrets, paths, logger, capabilities: { features: {} }, broadcast: () => {}, appRoot: root, }); const pork = registry.commands.get('backporks:pork'); assert.ok(pork); const queued = pork.handler({ game_id: 'PPSA54321', folder_name: '11.50.0', folder_path: backporkRoot }); await waitForJob(activeTransferState, queued.jobId); assert.equal(updates.length >= 1, true); assert.equal(updates.some(item => item.fields && Object.prototype.hasOwnProperty.call(item.fields, 'ftp_path')), true); const hasInstalled = updates.some(item => item.fields && Object.prototype.hasOwnProperty.call(item.fields, 'installed')); assert.equal(hasInstalled, false, 'backpork completion must not set installed'); } finally { fs.rmSync(root, { recursive: true, force: true }); activeTransferState = null; } }); });