'use strict'; const assert = require('node:assert/strict'); const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const test = require('node:test'); const { createPatchBundleService } = require('../../src/services/patch-bundle-service'); const { createAprAmprAdapterService, PINNED_UPSTREAM } = require('../../src/services/apr-ampr-adapter-service'); const digest = value => crypto.createHash('sha256').update(value).digest('hex'); function fixture(options = {}) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-apr-')); const source = path.join(root, 'source'); const artifacts = path.join(root, 'artifacts'); const workspace = path.join(root, 'workspace'); const staging = path.join(root, 'staging'); fs.mkdirSync(path.join(source, 'sce_module'), { recursive: true }); fs.mkdirSync(artifacts, { recursive: true }); fs.mkdirSync(workspace, { recursive: true }); fs.writeFileSync(path.join(source, 'sce_module', 'libSceAmpr.sprx'), 'OLD-AMPR'); fs.writeFileSync(path.join(source, 'ampr_emu.index'), 'OLD-INDEX'); fs.writeFileSync(path.join(source, 'asset.bin'), 'ASSET'); const artifactPath = path.join(artifacts, 'libSceAmpr.sprx'); fs.writeFileSync(artifactPath, 'PINNED-AMPR'); const patchService = createPatchBundleService({ sourceRoots: [source], bundleRoots: [workspace], stagingRoot: staging, now: () => 1_721_600_000_000 }); const adapter = createAprAmprAdapterService({ patchService, sourceRoots: [source, staging], artifact: { path: artifactPath, sha256: digest('PINNED-AMPR'), version: '0.2.5.3' }, workspaceRoot: workspace, supportedFirmwares: ['9.60'], simulationMode: true, now: () => 1_721_600_000_000, hooks: options.hooks }); const input = { sourceRoot: source, titleId: 'PPSA12345', version: '1.00', firmware: '9.60', loader: 'etaHEN', kstuffVersion: '1.07', storage: 'usb', format: 'folder', applicationId: 'apr-stage' }; return { root, source, staging, adapter, input, close: () => fs.rmSync(root, { recursive: true, force: true }) }; } test('simulated supported workflow detects requirement evidence and pins capabilities', async t => { const f = fixture(); t.after(f.close); const check = f.adapter.preflight(f.input); assert.equal(check.supported, true); assert.equal(check.detection.required, true); assert.match(check.detection.evidence.sha256, /^[a-f0-9]{64}$/); assert.equal(check.adapter.adapterVersion, '1.0.3'); assert.equal(check.adapter.gameCompressorRevision, PINNED_UPSTREAM.gameCompressorRevision); assert.equal(check.adapter.capabilities.liveConsoleMutation, false); assert.equal(check.mountSettingsProposal.applied, false); const staged = await f.adapter.stage(f.input); assert.equal(staged.state, 'verified'); assert.equal(staged.index.state, 'current'); assert.deepEqual(staged.lifecycle, ['detected', 'preflight-passed', 'index-rebuilding', 'staged', 'verified']); assert.equal(fs.readFileSync(path.join(f.source, 'sce_module', 'libSceAmpr.sprx'), 'utf8'), 'OLD-AMPR'); }); test('unsupported firmware, format and internal index bootstrap explain exact reasons', t => { const f = fixture(); t.after(f.close); const check = f.adapter.preflight({ ...f.input, firmware: '10.50', format: 'exfat' }); assert.equal(check.supported, false); assert.ok(check.reasons.some(item => item.code === 'APR_FIRMWARE_UNSUPPORTED')); assert.ok(check.reasons.some(item => item.code === 'APR_FORMAT_UNSUPPORTED')); fs.unlinkSync(path.join(f.source, 'ampr_emu.index')); const internal = f.adapter.preflight({ ...f.input, storage: 'internal' }); assert.ok(internal.reasons.some(item => item.code === 'APR_LOCATION_UNSUPPORTED')); }); test('index rebuild is deterministic, verified and source mismatch fails in staging', async t => { const good = fixture(); t.after(good.close); const staged = await good.adapter.stage(good.input); const document = JSON.parse(fs.readFileSync(path.join(staged.outputRoot, 'ampr_emu.index'), 'utf8')); assert.equal(document.format, 'chimera-ampr-index-simulator-v1'); assert.equal(document.consoleCompatible, false); assert.equal(document.entries[0].path, 'asset.bin'); const bad = fixture({ hooks: { beforeApply: ({ input }) => fs.writeFileSync(path.join(input.sourceRoot, 'sce_module', 'libSceAmpr.sprx'), 'CHANGED-AFTER-PREFLIGHT') } }); t.after(bad.close); await assert.rejects(bad.adapter.stage({ ...bad.input, applicationId: 'source-race' }), error => error.code === 'PATCH_SOURCE_MISMATCH'); assert.equal(fs.existsSync(path.join(bad.staging, 'source-race')), false); }); test('adapter rollback restores original staged module and index with verified lifecycle', async t => { const f = fixture(); t.after(f.close); const staged = await f.adapter.stage(f.input); const result = await f.adapter.rollback(f.input.applicationId); assert.equal(result.state, 'rolled-back'); assert.equal(result.outputsVerified, true); assert.deepEqual(result.lifecycle, ['rollback-preflight', 'rollback-staged', 'rolled-back']); assert.equal(fs.readFileSync(path.join(staged.outputRoot, 'sce_module', 'libSceAmpr.sprx'), 'utf8'), 'OLD-AMPR'); assert.equal(fs.readFileSync(path.join(staged.outputRoot, 'ampr_emu.index'), 'utf8'), 'OLD-INDEX'); });