61 lines
6.4 KiB
JavaScript
61 lines
6.4 KiB
JavaScript
'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);
|
|
});
|