Files
chimera-Public/test/server/ch-1401-p15-a7-route-selection-stage.test.js

174 lines
8.9 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-a7-route-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', 'old-loader.elf', '!30000', 'old-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.a7.stage.v1',
identity: 'a7-stage-test',
current: {
manifestSha256: digest(manifest), loaderName: 'old-loader.elf', loaderSize: oldLoader.length,
loaderSha256: digest(oldLoader), readerName: 'old-reader.elf', readerSize: oldReader.length, readerSha256: digest(oldReader),
},
next: {
loaderLocalName: 'new-loader.elf', loaderRemoteName: 'a7-loader.elf', loaderSize: newLoader.length, loaderSha256: digest(newLoader),
readerLocalName: 'new-reader.elf', readerRemoteName: 'a7-reader.elf', readerSize: newReader.length, readerSha256: digest(newReader),
},
currentManifest: currentLines,
};
const files = {
'autoload.txt': manifest, 'old-loader.elf': oldLoader, 'old-reader.elf': oldReader,
'manager.elf': elf(5), 'service.elf': elf(6),
};
return { local, temporary, backup, oldLoader, oldReader, newLoader, newReader, manifest, contract, files };
}
test('A7 stage plan is read-only and binds both exact replacements', async t => {
const f = await fixture(t);
const ftp = new FakeFtp(f.files);
const { createPlan } = await import('../../scripts/autonomous/p15-a7-stage-route-selection.mjs');
const plan = await createPlan({ ftp, localRoot: f.local, temporaryRoot: f.temporary, contract: f.contract });
assert.equal(ftp.writes, 0);
assert.match(plan.proposed.manifest, /a7-loader\.elf/);
assert.match(plan.proposed.manifest, /a7-reader\.elf/);
assert.doesNotMatch(plan.proposed.manifest, /old-loader\.elf|old-reader\.elf/);
assert.equal(plan.automaticRebootPerformed, false);
assert.equal(plan.launchPerformed, false);
});
test('A7 stage apply snapshots every file and preserves the A6 artifacts', async t => {
const f = await fixture(t);
const ftp = new FakeFtp(f.files);
const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a7-stage-route-selection.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: () => 'a7stage1',
});
assert.equal(receipt.status, 'staged_verified');
assert.equal(receipt.launchPerformed, false);
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/old-loader.elf`), f.oldLoader);
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/old-reader.elf`), f.oldReader);
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/a7-loader.elf`), f.newLoader);
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/a7-reader.elf`), f.newReader);
assert.equal(JSON.parse(fs.readFileSync(path.join(f.backup, receipt.snapshotId, 'meta.json'))).files.length, 5);
});
test('A7 independent staged readback verifies exact new and preserved old artifacts without writes', async t => {
const f = await fixture(t);
const ftp = new FakeFtp(f.files);
const { createPlan, applyPlan, verifyStaged, CONFIRMATION } = await import('../../scripts/autonomous/p15-a7-stage-route-selection.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.manifest.exactContent, true);
assert.equal(result.oldRuntimeArtifactsPreserved, true);
assert.equal(result.consoleMutationPerformed, false);
assert.equal(ftp.writes, writesBefore);
});
test('A7 independent staged readback fails closed on drift and unfinished publication files', async t => {
const f = await fixture(t);
const ftp = new FakeFtp({
...f.files,
'a7-loader.elf': f.newLoader,
'a7-reader.elf': f.newReader,
});
const { verifyStaged } = await import('../../scripts/autonomous/p15-a7-stage-route-selection.mjs');
await assert.rejects(verifyStaged({ ftp, temporaryRoot: f.temporary, contract: f.contract }), { code: 'P15_A7_STAGE_READBACK_DRIFT' });
ftp.files.set(`${REMOTE_DIR}/.chimera-unfinished.tmp`, Buffer.from('partial'));
await assert.rejects(verifyStaged({ ftp, temporaryRoot: f.temporary, contract: f.contract }), { code: 'P15_A7_STAGE_READBACK_TEMPORARY' });
});
test('A7 stage refuses missing confirmation and stale digest before writes', async t => {
const f = await fixture(t);
const ftp = new FakeFtp(f.files);
const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a7-stage-route-selection.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_A7_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_A7_STAGE_PLAN_DRIFT' });
assert.equal(ftp.writes, 0);
});
test('A7 stage post-read failure restores the manifest and removes both A7 targets', async t => {
const f = await fixture(t);
const ftp = new FakeFtp(f.files, true);
const { createPlan, applyPlan, CONFIRMATION } = await import('../../scripts/autonomous/p15-a7-stage-route-selection.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_A7_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}/a7-loader.elf`), false);
assert.equal(ftp.files.has(`${REMOTE_DIR}/a7-reader.elf`), false);
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/old-loader.elf`), f.oldLoader);
assert.deepEqual(ftp.files.get(`${REMOTE_DIR}/old-reader.elf`), f.oldReader);
});