import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import createModule from "../../build-wasm/linuxcnc-sim-core.js"; import { SimSession, CONTEXT_ID } from "./sim-session.mjs"; import { compareEndpoints, createEndpoint, endpointHash, firstDifference } from "./endpoint-replay.mjs"; const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)); async function sessionWithPlan(id, mode) { const core = await createModule({ wasmBinary }); const session = new SimSession(core, { sessionId: id }); const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: id }; let seq = 0; const send = (kind, operation_id, payload = {}) => { seq += 1; const response = session.accept({ ...base, kind, seq, operation_id, payload }); assert.notEqual(response.kind, "ERROR", `${id}:${kind}:${response.payload?.code ?? "error"}`); return response; }; send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace" }); send("HAL_COMMAND", "thread", { action: "thread", name: "servo-thread", period_ticks: 1 }); send("HAL_COMMAND", "component", { action: "component", instance: "gate", type: "and2", thread: "servo-thread" }); send("HAL_SET", "in0", { target: "pin", name: "gate.in0", value: 1 }); send("HAL_SET", "in1", { target: "pin", name: "gate.in1", value: 1 }); send("PLC_CONFIG", "plc", { rungs: [{ op: "SET", target: "ready", value: 1 }, { op: "ADD", target: "count", source: "ready" }] }); send("PROGRAM_LOAD", "program", { source: "G0 X1\nG1 X2\n", path: "replay.ngc" }); send("PROGRAM_RUN", "run"); if (mode === "single_step") { send("PROGRAM_STEP", "program-step"); } else if (mode === "1x") { for (let index = 0; index < 4; index += 1) send("SIM_STEP", `step-${index}`, { cycles: 1 }); } else { send("SIM_STEP", "step-fast", { cycles: 4 }); } return { session, send }; } const oneX = await sessionWithPlan("replay-1x", "1x"); const fast = await sessionWithPlan("replay-fast", "fast"); const oneEndpoint = createEndpoint({ session: oneX.session }); const fastEndpoint = createEndpoint({ session: fast.session }); const modeParity = compareEndpoints(oneEndpoint, fastEndpoint); assert.equal(modeParity.equal, true); assert.equal(modeParity.difference, null); const single = await sessionWithPlan("replay-single", "single_step"); const singleEndpoint = createEndpoint({ session: single.session }); assert.equal(singleEndpoint.task.program_status, "paused"); const replay = await sessionWithPlan("replay-duplicate", "1x"); const beforeDuplicate = createEndpoint({ session: replay.session }); const duplicateSeq = replay.session.timeline.events.cursor + 1; const duplicate = replay.session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: "replay-duplicate", seq: duplicateSeq, kind: "SIM_STEP", operation_id: "step-3", payload: { cycles: 1 } }); assert.equal(duplicate.replayed, true); const afterDuplicate = createEndpoint({ session: replay.session }); const duplicateParity = compareEndpoints(beforeDuplicate, afterDuplicate); assert.equal(duplicateParity.equal, true); const recoveredSource = await sessionWithPlan("replay-recover", "1x"); const checkpointResponse = recoveredSource.send("CHECKPOINT", "checkpoint"); assert.equal(checkpointResponse.kind, "CHECKPOINT_READY"); const checkpoint = checkpointResponse.payload.checkpoint; recoveredSource.send("SIM_STEP", "post-checkpoint", { cycles: 2 }); const continuousAfterCheckpoint = createEndpoint({ session: recoveredSource.session }); const recovery = recoveredSource.session.restoreCheckpoint(checkpoint, recoveredSource.session.timeline.export(), (operation) => operation); assert.equal(recovery.status, "recovered"); const recoveredEndpoint = createEndpoint({ session: recoveredSource.session }); const recoveryParity = compareEndpoints(continuousAfterCheckpoint, recoveredEndpoint); assert.equal(recoveryParity.equal, true); const trace = recoveredSource.session.exportTrace(); const traceCore = await createModule({ wasmBinary }); const traceSession = new SimSession(traceCore, { sessionId: "replay-recover" }); const imported = traceSession.importTrace(trace); assert.equal(imported.trace_hash, trace.trace_hash); const traceParity = compareEndpoints(recoveredEndpoint, createEndpoint({ session: traceSession })); assert.equal(traceParity.equal, true); const expected = { nested: [{ value: 1 }, { value: 2 }] }; const actual = { nested: [{ value: 1 }, { value: 3 }] }; assert.deepEqual(firstDifference(expected, actual), { path: "$.nested[1].value", expected: 2, actual: 3 }); const report = { schema: "endpoint-replay-s4-sample.v1", status: "verified", context_id: CONTEXT_ID, modes: { one_x: endpointHash(oneEndpoint), fast: endpointHash(fastEndpoint), single_step: endpointHash(singleEndpoint) }, parity: { mode_equal: modeParity, duplicate_operation_equal: duplicateParity, recovery_equal: recoveryParity, trace_equal: traceParity }, first_divergence: null, checkpoint_hash: checkpoint.checkpoint_hash, trace_hash: trace.trace_hash, }; console.log(JSON.stringify(report));