'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); value.writeUInt16LE(0x3e, 18); return value; } class FakeFtp { constructor(files, corrupt = false) { this.files = new Map(Object.entries(files).map(([name, bytes]) => [`${REMOTE_DIR}/${name}`, Buffer.from(bytes)])); this.writes = 0; this.corrupt = corrupt; } async list() { return [...this.files].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 new Error('missing'); if (this.corrupt && source.endsWith('/autoload.txt') && this.writes >= 5) 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'); this.files.delete(source); this.files.set(target, bytes); } async delete(target) { this.writes += 1; if (!this.files.delete(target)) throw new Error('missing'); } } async function fixture(t) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-a6-reader-stage-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const local = path.join(root, 'local'); const temporary = path.join(root, 'temp'); const backup = path.join(root, 'backup'); fs.mkdirSync(local); const loader = elf(1, 80); const oldReader = elf(2, 72); const newReader = elf(3, 76); const currentLines = ['', '', 'manager.elf', 'loader.elf', '!30000', 'old-reader.elf', '!5000', 'service.elf']; const manifest = Buffer.from(`${currentLines.join('\n')}\n`); fs.writeFileSync(path.join(local, 'new-reader.elf'), newReader); const contract = { schema: 'test.reader.stage.v1', identity: 'strict-reader-test', current: { manifestSha256: digest(manifest), loaderName: 'loader.elf', loaderSize: loader.length, loaderSha256: digest(loader), readerName: 'old-reader.elf', readerSize: oldReader.length, readerSha256: digest(oldReader) }, next: { localName: 'new-reader.elf', remoteName: 'strict-reader.elf', size: newReader.length, sha256: digest(newReader) }, currentManifest: currentLines, }; const files = { 'autoload.txt': manifest, 'loader.elf': loader, 'old-reader.elf': oldReader, 'manager.elf': elf(4), 'service.elf': elf(5) }; return { local, temporary, backup, loader, oldReader, newReader, manifest, contract, files }; } test('A6 strict-reader plan is read-only and changes only the reader manifest entry', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files); const { createPlan } = await import('../../scripts/autonomous/p15-a6-stage-semantic-reader.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, contract: f.contract }); assert.equal(ftp.writes, 0); assert.equal(plan.loaderChanged, false); assert.match(plan.proposed.manifest, /strict-reader\.elf/); assert.doesNotMatch(plan.proposed.manifest, /old-reader\.elf/); }); test('A6 strict-reader apply snapshots all files and preserves loader and prior reader', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files); const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a6-stage-semantic-reader.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, contract: f.contract }); const receipt = await applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, backupRoot: f.backup, contract: f.contract, confirmation: CONFIRMATION, planDigest: plan.planDigest, now: () => 1234567890123, nonce: () => 'reader01' }); assert.equal(receipt.status, 'staged_verified'); assert.equal(receipt.loaderChanged, false); assert.equal(receipt.launchPerformed, false); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/loader.elf`), f.loader); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/old-reader.elf`), f.oldReader); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/strict-reader.elf`), f.newReader); assert.equal(JSON.parse(fs.readFileSync(path.join(f.backup, receipt.snapshotId, 'meta.json'))).files.length, 5); }); test('A6 strict-reader refuses confirmation and plan drift before writes', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files); const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a6-stage-semantic-reader.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, contract: f.contract }); await assert.rejects(applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, backupRoot: f.backup, contract: f.contract, confirmation: 'wrong', planDigest: plan.planDigest }), { code: 'P15_A6_READER_CONFIRMATION' }); await assert.rejects(applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, backupRoot: f.backup, contract: f.contract, confirmation: CONFIRMATION, planDigest: '0'.repeat(64) }), { code: 'P15_A6_READER_PLAN_DRIFT' }); assert.equal(ftp.writes, 0); }); test('A6 strict-reader post-read failure restores manifest and removes new reader', async t => { const f = await fixture(t); const ftp = new FakeFtp(f.files, true); const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a6-stage-semantic-reader.mjs'); const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, contract: f.contract }); await assert.rejects(applyPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, backupRoot: f.backup, contract: f.contract, confirmation: CONFIRMATION, planDigest: plan.planDigest, now: () => 1234567890123, nonce: () => 'rollback' }), error => error.code === 'P15_A6_READER_POST_VERIFY' && error.details.rolledBack === true); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/autoload.txt`), f.manifest); assert.equal(ftp.files.has(`${REMOTE_DIR}/strict-reader.elf`), false); assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/loader.elf`), f.loader); });