180 lines
9.1 KiB
JavaScript
180 lines
9.1 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 >= 7) 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-a8-completion-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 oldLoader = elf(1, 80);
|
|
const oldReader = elf(2, 72);
|
|
const newLoader = elf(3, 96);
|
|
const newReader = elf(4, 76);
|
|
const currentLines = ['', '', 'manager.elf', 'a7-loader.elf', '!30000', 'a7-reader.elf', '!5000', 'service.elf'];
|
|
const manifest = Buffer.from(`${currentLines.join('\n')}\n`);
|
|
fs.writeFileSync(path.join(local, 'new-loader.elf'), newLoader);
|
|
fs.writeFileSync(path.join(local, 'new-reader.elf'), newReader);
|
|
const contract = {
|
|
schema: 'test.a8.stage.v1',
|
|
identity: 'a8-stage-test',
|
|
current: {
|
|
manifestSha256: digest(manifest), loaderName: 'a7-loader.elf', loaderSize: oldLoader.length,
|
|
loaderSha256: digest(oldLoader), readerName: 'a7-reader.elf', readerSize: oldReader.length,
|
|
readerSha256: digest(oldReader),
|
|
},
|
|
next: {
|
|
loaderLocalName: 'new-loader.elf', loaderRemoteName: 'a8-loader.elf', loaderSize: newLoader.length,
|
|
loaderSha256: digest(newLoader), readerLocalName: 'new-reader.elf', readerRemoteName: 'a8-reader.elf',
|
|
readerSize: newReader.length, readerSha256: digest(newReader),
|
|
},
|
|
currentManifest: currentLines,
|
|
};
|
|
const files = {
|
|
'autoload.txt': manifest, 'a7-loader.elf': oldLoader, 'a7-reader.elf': oldReader,
|
|
'manager.elf': elf(5), 'service.elf': elf(6),
|
|
};
|
|
return { local, temporary, backup, oldLoader, oldReader, newLoader, newReader, manifest, contract, files };
|
|
}
|
|
|
|
test('A8 plan is read-only and binds the exact call-bound runtime pair', async t => {
|
|
const f = await fixture(t);
|
|
const ftp = new FakeFtp(f.files);
|
|
const { createPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a8-stage-completion.mjs');
|
|
const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, contract: f.contract });
|
|
assert.equal(CONFIRMATION, 'STAGE CHIMERA P15 A8 COMPLETION RUNTIME');
|
|
assert.equal(ftp.writes, 0);
|
|
assert.match(plan.proposed.manifest, /a8-loader\.elf/);
|
|
assert.match(plan.proposed.manifest, /a8-reader\.elf/);
|
|
assert.doesNotMatch(plan.proposed.manifest, /a7-loader\.elf|a7-reader\.elf/);
|
|
assert.equal(plan.consoleMutationPerformed, false);
|
|
assert.equal(plan.payloadLoadPerformed, false);
|
|
assert.equal(plan.launchPerformed, false);
|
|
});
|
|
|
|
test('A8 default contract advances only the qualified A7 runtime and exact reproducible artifacts', async () => {
|
|
const { DEFAULT_CONTRACT } = await import('../../scripts/autonomous/p15-a8-stage-completion.mjs');
|
|
const currentManifest = Buffer.from(`${DEFAULT_CONTRACT.currentManifest.join('\n')}\n`);
|
|
assert.equal(digest(currentManifest), DEFAULT_CONTRACT.current.manifestSha256);
|
|
assert.equal(DEFAULT_CONTRACT.current.loaderName, 'kstuff-p15-a7-route-selection.elf');
|
|
assert.equal(DEFAULT_CONTRACT.current.readerName, 'kstuff-p15-a7-route-reader.elf');
|
|
assert.equal(DEFAULT_CONTRACT.next.loaderSha256, '9c1640cd61fdaa99f510f7eea78fe14ed526fc2ced8a78bef80c8bc03fed692f');
|
|
assert.equal(DEFAULT_CONTRACT.next.readerSha256, 'a2aedb429d2beb3e8bdb273329013db767c674c19d52854e5114dffa78f21699');
|
|
assert.equal(DEFAULT_CONTRACT.recoverySchema, 'chimera.p15-a8-completion-recovery.v1');
|
|
});
|
|
|
|
test('A8 apply snapshots the full autoload directory and preserves A7 rollback bytes', async t => {
|
|
const f = await fixture(t);
|
|
const ftp = new FakeFtp(f.files);
|
|
const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a8-stage-completion.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: () => 'a8stage1',
|
|
});
|
|
assert.equal(receipt.status, 'staged_verified');
|
|
assert.equal(receipt.consoleMutationPerformed, true);
|
|
assert.equal(receipt.automaticRebootPerformed, false);
|
|
assert.equal(receipt.payloadLoadPerformed, false);
|
|
assert.equal(receipt.launchPerformed, false);
|
|
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/a7-loader.elf`), f.oldLoader);
|
|
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/a7-reader.elf`), f.oldReader);
|
|
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/a8-loader.elf`), f.newLoader);
|
|
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/a8-reader.elf`), f.newReader);
|
|
assert.equal(JSON.parse(fs.readFileSync(path.join(f.backup, receipt.snapshotId, 'meta.json'))).files.length, 5);
|
|
});
|
|
|
|
test('A8 independent readback is read-only and verifies new plus preserved runtime bytes', async t => {
|
|
const f = await fixture(t);
|
|
const ftp = new FakeFtp(f.files);
|
|
const { createPlan, applyPlan, verifyStaged, CONFIRMATION } = await import('../../scripts/autonomous/p15-a8-stage-completion.mjs');
|
|
const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, contract: f.contract });
|
|
await applyPlan({
|
|
ftp, localRoot: f.local, temporaryRoot: f.temporary, backupRoot: f.backup, contract: f.contract,
|
|
confirmation: CONFIRMATION, planDigest: plan.planDigest, now: () => 1234567890123, nonce: () => 'readback',
|
|
});
|
|
const writesBefore = ftp.writes;
|
|
const result = await verifyStaged({ ftp, temporaryRoot: f.temporary, contract: f.contract });
|
|
assert.equal(result.status, 'verified');
|
|
assert.equal(result.oldRuntimeArtifactsPreserved, true);
|
|
assert.equal(result.consoleMutationPerformed, false);
|
|
assert.equal(ftp.writes, writesBefore);
|
|
});
|
|
|
|
test('A8 refuses confirmation or plan drift before any write', async t => {
|
|
const f = await fixture(t);
|
|
const ftp = new FakeFtp(f.files);
|
|
const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a8-stage-completion.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_A8_STAGE_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_A8_STAGE_PLAN_DRIFT' });
|
|
assert.equal(ftp.writes, 0);
|
|
});
|
|
|
|
test('A8 post-publication drift restores A7 manifest and removes A8 targets', async t => {
|
|
const f = await fixture(t);
|
|
const ftp = new FakeFtp(f.files, true);
|
|
const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a8-stage-completion.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_A8_STAGE_POST_VERIFY' && error.details.rolledBack === true);
|
|
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/autoload.txt`), f.manifest);
|
|
assert.equal(ftp.files.has(`${REMOTE_DIR}/a8-loader.elf`), false);
|
|
assert.equal(ftp.files.has(`${REMOTE_DIR}/a8-reader.elf`), false);
|
|
});
|