Files
linuxcncWebWASM/web/worker/test_trt_checkpoint_exact.mjs

145 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile, mkdir, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
import { MACHINE_DEFINITIONS } from "../workspace/machine-definitions.mjs";
import { fnv1a64 } from "./replay.mjs";
const root = new URL("../../", import.meta.url);
const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root));
const fetchImpl = async (url) => {
try { const text = await readFile(new URL(String(url).replace(/^\//, ""), root), "utf8"); return { ok: true, text: async () => text }; }
catch { return { ok: false }; }
};
async function open(machine) {
const bundle = await loadSimulationCase({ path: machine.entrypoint, fetchImpl });
const subroutine = machine.subroutines[0];
const subPath = machine.entrypoint.replace(/[^/]+$/, `remap_subs/${subroutine}.ngc`);
bundle.files[subPath] = `O<${subroutine}> sub\n#31=[#31+#1]\nG1 X#31 Y4 Z2 ${machine.coordinates[3]}8 C12 F500\nO<${subroutine}> endsub\n`;
const core = await createModule({ wasmBinary });
// Give each module a different heap layout; native restore must rebind pointers.
core._malloc(4096 + rows.length * 128);
const session = new SimSession(core, { sessionId: `exact-${machine.id}`, planner: "linuxcnc-tp" });
let seq = 0;
const send = (kind, payload = {}, operationId = null) => {
const result = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: operationId ?? `exact-${seq}`, payload });
assert.notEqual(result.kind, "ERROR", `${kind}: ${result.payload?.message_key ?? result.payload?.code ?? JSON.stringify({ preview: result.payload?.preview?.code, line: result.payload?.preview?.line, program_error: result.payload?.program?.last_error })}`);
return result;
};
send("HELLO"); send("SESSION_ATTACH"); send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
send("POWER", { active: true }); send("HOME", { all: true });
for (let i = 0; i < 500 && !session.wasmHomeLimit.read().state.homed; i++) send("SIM_STEP", { cycles: 1 });
assert.ok(session.wasmHomeLimit.read().state.homed);
return { session, core, send, adoptSequence: (value) => { seq = value; } };
}
const rows = [];
for (const machine of MACHINE_DEFINITIONS) {
const original = await open(machine);
const idle = original.send("CHECKPOINT").payload.checkpoint;
const idleRestored = await open(machine);
assert.equal(idleRestored.session.restoreCheckpoint(idle).status, "recovered");
assert.equal(fnv1a64(idleRestored.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), "idle machine without a loaded program");
const rotary = machine.coordinates[3];
const source = `G21 G90 G64\nM428\n#31=3\nG1 X6 Y0 Z2 ${rotary}5 C8 F600\nG3 X0 Y6 I-6 J0 ${rotary}10 C16\nO<${machine.subroutines[0]}> call [3]\nM429\nG1 X2 Y3 Z4 F600\nM2\n`;
original.send("PROGRAM_LOAD", { source, path: machine.program.replace(/[^/]+$/, "checkpoint-exact.ngc") }); original.send("PROGRAM_RUN");
const seen = new Set(); let samples = 0;
for (let index = 0; index < 500; index++) {
original.send("SIM_STEP", { cycles: 5, response: "motion_delta" });
const current = original.session.wasmMotion.read().state;
const program = original.session.xyzacProgram.snapshot();
const phase = program.call_depth > 0 ? "subroutine" : current.motion_type === 3 ? "arc" : current.queue_depth > 0 ? "line" : program.program_status === "completed" ? "completed" : "barrier";
if (!seen.has(phase)) {
seen.add(phase);
const checkpoint = original.send("CHECKPOINT").payload.checkpoint;
assert.ok(checkpoint?.state.program.native_state);
assert.ok(!JSON.stringify(checkpoint.state.ledger).includes('"checkpoint":'), "durable ledger must not recursively contain earlier checkpoints");
const restarted = await open(machine);
restarted.session.workspaceFiles[machine.entrypoint.replace(/[^/]+$/, `remap_subs/${machine.subroutines[0]}.ngc`)] = "unrelated new subroutine contents";
const result = restarted.session.restoreCheckpoint(checkpoint);
assert.equal(result.status, "recovered", JSON.stringify(result.blocked));
restarted.adoptSequence(checkpoint.event_cursor);
assert.equal(fnv1a64(restarted.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), `${machine.id} ${phase}: initial state`);
for (let step = 0; step < 12; step++) {
original.send("SIM_STEP", { cycles: 1, response: "motion_delta" }); restarted.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
const expected = original.session.workspacePackageState(), actual = restarted.session.workspacePackageState();
assert.equal(fnv1a64(actual), fnv1a64(expected), `${machine.id} ${phase}: divergent tick ${original.session.tick}`);
samples++;
}
rows.push({ machine: machine.id, phase, checkpoint_tick: checkpoint.simulation_tick, continuation_tick: original.session.tick, state_hash: fnv1a64(original.session.workspacePackageState()), samples: 12, first_divergent_tick: null });
if (phase === "subroutine") {
original.send("PROGRAM_PAUSE");
const paused = original.send("CHECKPOINT").payload.checkpoint;
const resumed = await open(machine);
assert.equal(resumed.session.restoreCheckpoint(paused).status, "recovered");
resumed.adoptSequence(paused.event_cursor);
const transported = await open(machine);
assert.equal(transported.send("RECOVER", { checkpoint: paused }).payload.status, "recovered");
original.send("SIM_STEP", { cycles: 2 }); resumed.send("SIM_STEP", { cycles: 2 });
assert.equal(fnv1a64(resumed.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), "paused state must stay unchanged");
original.send("PROGRAM_RESUME"); resumed.send("PROGRAM_RESUME"); transported.send("PROGRAM_RESUME");
for (let step = 0; step < 12; step++) {
original.send("SIM_STEP", { cycles: 1, response: "motion_delta" }); resumed.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
transported.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
assert.equal(fnv1a64(resumed.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), `paused resume divergent tick ${original.session.tick}`);
assert.deepEqual(transported.session.wasmMotion.read(), original.session.wasmMotion.read(), "new transport must continue the same physical trajectory");
}
rows.push({ machine: machine.id, phase: "paused-resumed", checkpoint_tick: paused.simulation_tick, continuation_tick: original.session.tick, state_hash: fnv1a64(original.session.workspacePackageState()), samples: 12, new_transport_recovery: true, first_divergent_tick: null });
}
}
if (original.session.xyzacProgram.snapshot().program_status === "completed" && original.session.wasmMotion.read().state.queue_depth === 0) break;
}
assert.equal(original.session.xyzacProgram.snapshot().program_status, "completed");
assert.ok(seen.has("line") && seen.has("arc"), [...seen].join(","));
assert.ok(samples >= 24);
const trace = original.session.exportTrace();
const imported = await open(machine);
assert.equal(imported.session.importTrace(trace).status, "verified");
assert.equal(fnv1a64(imported.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), "native trace import");
const checkpoint = original.send("CHECKPOINT").payload.checkpoint;
const invalid = structuredClone(checkpoint);
invalid.state.wasm_motion.native_state.blocks[0][1] = "invalid";
invalid.state_hash = fnv1a64(invalid.state);
invalid.checkpoint_hash = fnv1a64(Object.fromEntries(Object.entries(invalid).filter(([key]) => key !== "checkpoint_hash")));
const unchanged = fnv1a64(original.session.workspacePackageState());
assert.equal(original.session.restoreCheckpoint(invalid).status, "blocked");
assert.equal(fnv1a64(original.session.workspacePackageState()), unchanged, "corrupt native blob rejected before mutation");
const foreignBuild = structuredClone(checkpoint);
foreignBuild.state.wasm_motion.native_state.build_id = "0".repeat(64);
foreignBuild.state_hash = fnv1a64(foreignBuild.state);
foreignBuild.checkpoint_hash = fnv1a64(Object.fromEntries(Object.entries(foreignBuild).filter(([key]) => key !== "checkpoint_hash")));
const rejectedBuild = original.session.restoreCheckpoint(foreignBuild);
assert.equal(rejectedBuild.status, "blocked");
assert.match(rejectedBuild.blocked[0].reason, /checkpoint_build_mismatch/);
assert.equal(fnv1a64(original.session.workspacePackageState()), unchanged);
const transported = await open(machine);
const recovery = transported.send("RECOVER", { checkpoint });
assert.equal(recovery.payload.status, "recovered", JSON.stringify(recovery.payload));
const next = transported.send("SIM_STEP", { cycles: 1 });
assert.equal(next.payload.program.program_status, "completed", "transport sequence continues after recovery");
assert.deepEqual(next.payload.wasm_motion.state.position, original.session.wasmMotion.read().state.position);
rows.push({ machine: machine.id, phase: "trace-and-transport", state_hash: fnv1a64(original.session.workspacePackageState()), first_divergent_tick: null });
transported.send("SET_MODE", { mode: "MANUAL" });
transported.send("PROGRAM_LOAD", { source: "G21 G90\nM2\n", path: "jog-after-recovery.ngc" });
transported.send("JOG_START", { axis: "x", direction: 1, increment: .1 });
let jogSamples = 0;
while (transported.session.wasmMotion.read().state.queue_depth > 0 && jogSamples < 100) {
const delta = transported.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
assert.deepEqual(delta.payload.motion_sample.joints, transported.session.wasmMotion.read().state.joint_commanded, "identity motion delta must use the current planner joints");
jogSamples++;
}
assert.equal(transported.session.wasmMotion.read().state.queue_depth, 0);
rows.push({ machine: machine.id, phase: "post-recovery-jog", samples: jogSamples, first_divergent_tick: null });
const beforeRewind = transported.session.wasmMotion.read();
const rewind = transported.send("RECOVER", { checkpoint: idle });
assert.equal(rewind.payload.status, "blocked");
assert.equal(rewind.payload.code, "runtime_checkpoint_requires_new_session");
assert.deepEqual(transported.session.wasmMotion.read(), beforeRewind);
}
const report = { report_id: "trt-checkpoint-exact-v1", status: "verified", safeguards: { corrupt_native_rejected: true, foreign_build_rejected: true, frozen_subroutines: true, heap_relocation_checked: true, receipts_nonrecursive: true, backward_transport_restore_rejected: true }, rows, state_hash: fnv1a64(rows) };
const output = process.env.TRT_EVIDENCE_DIR ? pathToFileURL(`${process.env.TRT_EVIDENCE_DIR.replace(/\/$/, "")}/`) : new URL("coverage/recovery/", root);
await mkdir(output, { recursive: true });
await writeFile(new URL("exact-checkpoint.report.json", output), JSON.stringify(report, null, 2) + "\n");
console.log(JSON.stringify(report));