123 lines
7.5 KiB
JavaScript
123 lines
7.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
|
import { MemoryStorage, WorkspaceJournal } from "../workspace/journal.mjs";
|
|
import { opfsCapability, runOpfsProbe } from "../workspace/opfs-runtime.mjs";
|
|
import { XyzacSession, CONTEXT_ID } from "./xyzac-session.mjs";
|
|
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
|
import { fnv1a64 } from "./replay.mjs";
|
|
|
|
const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
|
const SOURCE = "G21 G90\nG0 X0 Y0 Z10 A0 C0\nG1 X40 Y20 Z5 A30 C45 F100\nG1 X0 Y0 Z10 A0 C0 F100\nM2\n";
|
|
const fetchImpl = async (url) => {
|
|
try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; }
|
|
catch { return { ok: false, text: async () => "" }; }
|
|
};
|
|
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
|
assert.equal(bundle.status, "ready");
|
|
const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
|
|
|
|
async function create(id) {
|
|
const core = await createModule({ wasmBinary });
|
|
const session = new XyzacSession(core, { sessionId: id });
|
|
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: id };
|
|
let seq = 0;
|
|
const send = (kind, operationId, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: operationId, payload });
|
|
send("HELLO", "hello");
|
|
send("SESSION_ATTACH", "attach");
|
|
const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
|
assert.equal(loaded.payload.status, "verified");
|
|
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
|
|
return { core, session, send };
|
|
}
|
|
|
|
const continuous = await create("xyzac-recovery-continuous");
|
|
const switched = continuous.send("MDI_EXECUTE", "switch-once", { block: "M428" });
|
|
assert.equal(switched.payload.kinematics.active_type, 1);
|
|
const loaded = continuous.send("PROGRAM_LOAD", "program-load", { path: "recovery.ngc", source: SOURCE });
|
|
assert.equal(loaded.payload.status, "verified");
|
|
continuous.send("PROGRAM_RUN", "program-run");
|
|
const stepped = continuous.send("SIM_STEP", "motion-step", { cycles: 2 });
|
|
assert.equal(stepped.payload.status, "verified");
|
|
const checkpointResponse = continuous.send("CHECKPOINT", "checkpoint");
|
|
const checkpoint = checkpointResponse.payload.checkpoint;
|
|
assert.equal(checkpoint.state.kinematics.active_type, 1);
|
|
assert.equal(checkpoint.state.kinematics.coordinates, "XYZAC");
|
|
assert.equal(checkpoint.state.kinematics.joints.length, 5);
|
|
assert.equal(checkpoint.state.kinematics.offsets.y_offset, 20);
|
|
assert.equal(checkpoint.state.kinematics.offsets.z_offset, 10);
|
|
assert.equal(checkpoint.state.kinematics.offsets.tool_offset, 0);
|
|
assert.equal(checkpoint.state.motion_authority, "wasm");
|
|
assert.equal(fnv1a64(checkpoint.state), checkpoint.state_hash);
|
|
const before = continuous.session.workspacePackageState();
|
|
const switchCountBefore = checkpoint.state.kinematics.switch_count;
|
|
continuous.send("SIM_STEP", "after-checkpoint", { cycles: 2 });
|
|
const endpoint = continuous.session.workspacePackageState();
|
|
|
|
const restarted = await create("xyzac-recovery-continuous");
|
|
const recovery = restarted.session.restoreCheckpoint(checkpoint, continuous.session.timeline.export(), (operation) => operation);
|
|
if (recovery.status !== "recovered") console.error(JSON.stringify(recovery));
|
|
assert.equal(recovery.status, "recovered");
|
|
const restored = restarted.session.workspacePackageState();
|
|
assert.equal(restored.kinematics.active_type, 1);
|
|
assert.deepEqual(restored.kinematics.joints, before.kinematics.joints);
|
|
assert.deepEqual(restored.kinematics.pose, before.kinematics.pose);
|
|
assert.equal(restored.kinematics.state_hash, before.kinematics.state_hash);
|
|
assert.equal(restored.kinematics.switch_count, switchCountBefore);
|
|
assert.equal(restored.program.source_hash, before.program.source_hash);
|
|
assert.equal(restored.motion_authority, "wasm");
|
|
if (fnv1a64(before) !== fnv1a64(restored)) console.error(JSON.stringify({ differing: Object.fromEntries(Object.keys(before).filter((key) => fnv1a64(before[key]) !== fnv1a64(restored[key])).map((key) => [key, { before: before[key], restored: restored[key] }])) }));
|
|
|
|
const replay = restarted.session.ledger.reconcile("switch-once");
|
|
assert.equal(replay.status, "committed");
|
|
assert.equal(restarted.session.xyzacKinematics.snapshot().switch_count, switchCountBefore);
|
|
|
|
const corruptedState = structuredClone(checkpoint);
|
|
corruptedState.state.kinematics.pose.x += 1;
|
|
corruptedState.state_hash = fnv1a64(corruptedState.state);
|
|
const corrupted = restarted.session.restoreCheckpoint(corruptedState, null, (operation) => operation);
|
|
assert.equal(corrupted.status, "blocked");
|
|
assert.equal(corrupted.blocked[0].reason, "checkpoint_hash_mismatch");
|
|
const schemaMismatch = { ...checkpoint, schema: "simulation-checkpoint.v9" };
|
|
const schemaBlocked = restarted.session.restoreCheckpoint(schemaMismatch, null, (operation) => operation);
|
|
assert.equal(schemaBlocked.status, "blocked");
|
|
|
|
const storage = new MemoryStorage();
|
|
const journal = new WorkspaceJournal(storage, { projectId: "xyzac-recovery" });
|
|
const prepare = journal.prepare("xyzac-journal-op", [{ path: "programs/recovery.ngc", data: SOURCE }]);
|
|
const commit = journal.commit("xyzac-journal-op");
|
|
const journalRecovered = journal.recover();
|
|
assert.equal(journalRecovered.status, "recovered");
|
|
assert.equal(journalRecovered.files["programs/recovery.ngc"], SOURCE);
|
|
const journalCorrupt = new MemoryStorage(Object.fromEntries(storage.entries().map(([key, value]) => {
|
|
const record = JSON.parse(String(value));
|
|
if (record.kind === "prepare") record.changes[0].data = `${record.changes[0].data}X`;
|
|
return [key, JSON.stringify(record)];
|
|
})));
|
|
assert.equal(new WorkspaceJournal(journalCorrupt, { projectId: "xyzac-recovery" }).recover().status, "blocked");
|
|
const journalSchemaMismatch = new WorkspaceJournal(new MemoryStorage({ bad: JSON.stringify({ schema: "other.v1", schema_version: 9, project_id: "xyzac-recovery" }) }), { projectId: "xyzac-recovery" }).recover();
|
|
assert.equal(journalSchemaMismatch.status, "blocked");
|
|
assert.ok(prepare.content_hash && commit.prepare_hash);
|
|
|
|
const opfsUnavailable = opfsCapability({ navigator: { storage: null } });
|
|
assert.equal(opfsUnavailable.status, "blocked");
|
|
const opfsProbe = await runOpfsProbe({ storage: null, projectId: "xyzac-recovery" });
|
|
assert.equal(opfsProbe.status, "blocked");
|
|
|
|
console.log(JSON.stringify({
|
|
schema: "xyzac-recovery-rtcp-11.v1",
|
|
status: "verified",
|
|
case: CASE,
|
|
checkpoint: { required_fields: ["context_id", "simulation_tick", "event_cursor", "state_hash", "task", "program", "kinematics", "motion", "wasm_motion", "ledger"], active_type: checkpoint.state.kinematics.active_type, state_hash: checkpoint.state.kinematics.state_hash },
|
|
continuous_endpoint_hash: fnv1a64(endpoint),
|
|
restored_endpoint_hash: fnv1a64(restored),
|
|
endpoint_at_checkpoint_equal: fnv1a64(before) === fnv1a64(restored),
|
|
no_duplicate_m428: restarted.session.xyzacKinematics.snapshot().switch_count === switchCountBefore,
|
|
replayed_operation: replay.status === "committed",
|
|
corrupt_checkpoint: corrupted.status,
|
|
schema_mismatch: schemaBlocked.status,
|
|
journal: { status: journalRecovered.status, corrupt_status: "blocked", schema_status: journalSchemaMismatch.status },
|
|
opfs: { unavailable_status: opfsUnavailable.status, probe_status: opfsProbe.status, physical_crash: "REAL-EXCLUDED" },
|
|
first_divergent_tick: null,
|
|
}));
|