Files
chimera-Public/test/server/ch-1401-ps5-fpkg-safe-runtime-stage.test.js

108 lines
7.5 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, 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);
});