'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 REMOTE_DIR = '/data/ps5_autoloader'; const digest = value => crypto.createHash('sha256').update(value).digest('hex'); function elf(marker, size = 64) { const value = Buffer.alloc(size, marker); Buffer.from('7f454c46020101', 'hex').copy(value, 0); value.writeUInt16LE(0x3e, 18); return value; } function contractFor({ currentLoader, currentReader, safeLoader, safeReader, manifest }) { return { schema: 'test.stage.v1', identity: 'safe-test', current: { manifestSha256: digest(manifest), loaderName: 'old-loader.elf', loaderSha256: digest(currentLoader), readerName: 'old-reader.elf', readerSha256: digest(currentReader), }, safe: { loaderLocalName: 'safe-loader.elf', loaderRemoteName: 'new-loader.elf', loaderSize: safeLoader.length, loaderSha256: digest(safeLoader), readerLocalName: 'safe-reader.elf', readerRemoteName: 'new-reader.elf', readerSize: safeReader.length, readerSha256: digest(safeReader), }, currentManifest: manifest.toString('utf8').trimEnd().split('\n'), }; } class FakeFtp { constructor(files, { corruptPostManifest = false } = {}) { this.files = new Map(Object.entries(files).map(([name, bytes]) => [`${REMOTE_DIR}/${name}`, Buffer.from(bytes)])); this.writes = 0; this.corruptPostManifest = corruptPostManifest; } async list() { return [...this.files.entries()].map(([name, bytes]) => ({ name: path.posix.basename(name), size: bytes.length, isDir: false })); } async download(source, target) { let bytes = this.files.get(source); if (!bytes) throw Object.assign(new Error('missing'), { code: 550 }); if (this.corruptPostManifest && source.endsWith('/autoload.txt') && this.writes >= 6) bytes = Buffer.from('corrupt\n'); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, bytes); } async upload(source, target) { this.writes += 1; this.files.set(target, fs.readFileSync(source)); } async rename(source, target) { this.writes += 1; const bytes = this.files.get(source); if (!bytes) throw new Error('missing rename source'); this.files.delete(source); this.files.set(target, bytes); } async delete(target) { this.writes += 1; if (!this.files.delete(target)) throw new Error('missing delete target'); } } async function fixture(t) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-a6-stage-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const local = path.join(root, 'local'); const temp = path.join(root, 'temp'); const backup = path.join(root, 'backup'); fs.mkdirSync(local, { recursive: true }); const currentLoader = elf(1); const currentReader = elf(2); const safeLoader = elf(3, 80); const safeReader = elf(4, 72); const manifest = Buffer.from('manager.elf\nold-loader.elf\n!30000\nold-reader.elf\nservice.elf\n'); fs.writeFileSync(path.join(local, 'safe-loader.elf'), safeLoader); fs.writeFileSync(path.join(local, 'safe-reader.elf'), safeReader); const contract = contractFor({ currentLoader, currentReader, safeLoader, safeReader, manifest }); const files = { 'autoload.txt': manifest, 'old-loader.elf': currentLoader, 'old-reader.elf': currentReader, 'manager.elf': elf(5), 'service.elf': elf(6) }; return { root, local, temp, backup, currentLoader, currentReader, safeLoader, safeReader, manifest, contract, files }; } test('A6 safe runtime plan binds current and proposed hashes without remote writes', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files); const { createPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a6-stage-safe-runtime.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, contract: f.contract }); assert.equal(plan.available, true); assert.equal(plan.confirmation, CONFIRMATION); assert.equal(ftp.writes, 0); assert.equal(plan.evidence.currentManifestSha256, digest(f.manifest)); assert.match(plan.proposed.manifest, /new-loader\.elf/); assert.doesNotMatch(plan.proposed.manifest, /old-loader\.elf/); }); test('A6 safe runtime apply snapshots all files, atomically replaces only the manifest and preserves old artifacts', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files); const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a6-stage-safe-runtime.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, contract: f.contract }); const receipt = await applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, backupRoot: f.backup, contract: f.contract, confirmation: CONFIRMATION, planDigest: plan.planDigest, now: () => 1234567890123, nonce: () => 'abc123' }); assert.equal(receipt.status, 'staged_verified'); assert.equal(receipt.automaticRebootPerformed, false); assert.equal(receipt.launchPerformed, false); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/old-loader.elf`), f.currentLoader); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/old-reader.elf`), f.currentReader); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/new-loader.elf`), f.safeLoader); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/new-reader.elf`), f.safeReader); assert.match(ftp.files.get(`${REMOTE_DIR}/autoload.txt`).toString('utf8'), /new-loader\.elf/); const snapshot = path.join(f.backup, receipt.snapshotId); assert.equal(fs.existsSync(path.join(snapshot, 'meta.json')), true); assert.equal(fs.existsSync(path.join(snapshot, 'receipt.json')), true); assert.equal(JSON.parse(fs.readFileSync(path.join(snapshot, 'meta.json'), 'utf8')).files.length, 5); }); test('A6 safe runtime refuses missing confirmation or stale plan before remote mutation', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files); const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a6-stage-safe-runtime.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, contract: f.contract }); await assert.rejects(applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, backupRoot: f.backup, contract: f.contract, confirmation: 'wrong', planDigest: plan.planDigest }), { code: 'P15_A6_CONFIRMATION' }); await assert.rejects(applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, backupRoot: f.backup, contract: f.contract, confirmation: CONFIRMATION, planDigest: '0'.repeat(64) }), { code: 'P15_A6_PLAN_DRIFT' }); assert.equal(ftp.writes, 0); }); test('A6 safe runtime post-read failure restores the original manifest and removes published safe files', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files, { corruptPostManifest: true }); const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a6-stage-safe-runtime.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, contract: f.contract }); await assert.rejects(applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temp, backupRoot: f.backup, contract: f.contract, confirmation: CONFIRMATION, planDigest: plan.planDigest, now: () => 1234567890123, nonce: () => 'rollback' }), error => error.code === 'P15_A6_POST_VERIFY' && error.details.rolledBack === true); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/autoload.txt`), f.manifest); assert.equal(ftp.files.has(`${REMOTE_DIR}/new-loader.elf`), false); assert.equal(ftp.files.has(`${REMOTE_DIR}/new-reader.elf`), false); });