import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import createModule from "../../build-wasm/linuxcnc-sim-core.js"; import { XyzacSession, CONTEXT_ID } from "./xyzac-session.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs"; const bundle = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", fetchImpl: async (url) => { try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url), "utf8"); return { ok: true, text: async () => body }; } catch { return { ok: false, text: async () => "" }; } } }); const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) }); const session = new XyzacSession(core, { sessionId: "pause-consistency" }); let seq = 0; const send = (kind, payload = {}, accepted = true) => { const result = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, payload, seq: ++seq, operation_id: `pause-consistency-${seq}` }); assert.equal(result.kind === "ERROR", !accepted, `${kind}: ${result.payload.gate_reason ?? result.payload.message_key ?? result.payload.code}`); assert.equal(core._sim_protocol_last_seq(), seq, `${kind} stranded the transport`); assert.equal(session.timeline.events.cursor, seq, `${kind} stranded the event log`); if (!accepted) assert.equal(result.payload.code, "axis_gate"); return result.payload; }; const motion = () => session.wasmMotion.read().state; const snapshot = () => session.xyzacProgram.snapshot(); const sample = (cycles = 5) => send("SIM_STEP", { cycles, response: "motion_delta" }); send("HELLO"); send("SESSION_ATTACH"); send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }); send("PROGRAM_LOAD", { path: "pause-reading.ngc", source: "G21 G90\n" + Array.from({ length: 80 }, (_, i) => `G1 X${i % 2 ? 0 : 2} F600`).join("\n") + "\nM2\n" }); send("POWER", { active: true }); send("HOME", { all: true }); sample(1); send("PROGRAM_RUN"); sample(); for (let round = 0; round < 3; round++) { send("PROGRAM_PAUSE"); const pose = motion().position, line = snapshot().current_line, queue = motion().queue_depth; const epoch = session.livePathEpoch; sample(); assert.deepEqual(motion().position, pose); send("VISUAL_CLEAR"); assert.equal(session.livePathEpoch, epoch + 1); for (const [kind, payload] of [["SET_SPINDLE_OVERRIDE", { scale: 1 }], ["SET_MAX_VELOCITY", { percent: 100 }], ["SET_JOG_SCALE", { percent: 50 }], ["SET_OVERRIDE", { feed: 1 }]]) send(kind, payload); for (const [kind, payload] of [["PROGRAM_PAUSE", {}], ["PROGRAM_RUN", {}], ["PROGRAM_LOAD", { path: "bad.ngc", source: "M2\n" }], ["MDI_EXECUTE", { block: "G0 X100" }], ["HOME", { all: true }], ["JOG_START", { axis: "x", direction: 1, increment: 1 }]]) send(kind, payload, false); assert.equal(snapshot().current_line, line); assert.equal(snapshot().program_status, "paused"); assert.deepEqual(motion().position, pose); assert.equal(motion().queue_depth, queue); send("PROGRAM_RESUME"); for (let n = 0; n < 3; n++) sample(); assert.notDeepEqual(motion().position, pose, "Resume did not advance the preserved TP"); send("PROGRAM_RESUME", {}, false); sample(); } send("PROGRAM_PAUSE"); const beforeStep = motion().position; send("PROGRAM_STEP"); for (let tick = 0; tick < 300 && session.wasmMotion.isStepping(); tick++) sample(1); assert.notDeepEqual(motion().position, beforeStep, "Step while paused did not move"); assert.equal(snapshot().program_status, "paused"); send("VISUAL_CLEAR"); send("PROGRAM_RESUME"); sample(); send("PROGRAM_ABORT"); assert.equal(motion().queue_depth, 0); send("PROGRAM_LOAD", { path: "pause-waiting.ngc", source: "G21 G90\nG1 X30 F600\nM2\n" }); send("PROGRAM_RUN"); for (let tick = 0; tick < 30 && snapshot().status !== 4; tick++) sample(1); assert.equal(snapshot().status, 4); assert.ok(motion().queue_depth > 0); send("PROGRAM_PAUSE"); send("VISUAL_CLEAR"); send("PROGRAM_RESUME"); assert.equal(snapshot().status, 4, "WAITING resume must not rewind or re-read"); for (let tick = 0; tick < 1000 && motion().queue_depth; tick++) sample(); assert.ok(Math.abs(motion().position.x - 30) < 1e-8); send("PROGRAM_LOAD", { path: "pause-mdi.ngc", source: "G21 G90\nM2\n" }); send("MDI_EXECUTE", { block: "G1 X20 F600" }); sample(); send("PROGRAM_PAUSE"); const mdiPose = motion().position; send("SET_SPINDLE_OVERRIDE", { scale: .8 }); send("VISUAL_CLEAR"); send("PROGRAM_RESUME"); sample(); assert.notDeepEqual(motion().position, mdiPose); assert.equal(session.task.state.mode, "MDI"); send("PROGRAM_ABORT"); console.log(JSON.stringify({ schema: "xyzac-pause-consistency.v1", status: "verified", commands: seq, transport_seq: core._sim_protocol_last_seq(), checks: ["reading_repeated_pause", "clear_and_overrides_while_paused", "rejected_commands_keep_sequence", "paused_step_then_resume", "waiting_resume_to_completion", "mdi_pause_resume", "abort_after_resume"] }));