import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import createModule from "../../build-wasm/linuxcnc-sim-core.js"; import { AxisOperationController } from "../app/axis-operation-controller.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs"; import { XyzacSession, CONTEXT_ID } from "./xyzac-session.mjs"; const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"; const PROGRAM = "G21 G90\nG0 X0 Y0 Z10 A0 C0\nG1 X10 Y5 Z5 A30 C45 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 core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) }); const session = new XyzacSession(core, { sessionId: "xyzac-protocol-react" }); const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId }; 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"); assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified"); const power = send("POWER", "power-on", { active: true }); assert.equal(power.kind, "COMMAND_RESULT"); assert.equal(power.payload.command, "POWER"); assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true); const switches = []; for (const type of [0, 1, 2, 1]) { const response = send("KINEMATICS_SELECT", `select-${type}-${seq}`, { type }); assert.equal(response.kind, "COMMAND_RESULT"); assert.equal(response.payload.kinematics.active_type, type); assert.equal(response.payload.remap.hal_pin, "motion.switchkins-type"); switches.push({ type, operation_id: response.operation_id, phase: response.payload.xyzac_ledger?.entries?.at(-1)?.phase ?? "committed" }); } const invalid = send("KINEMATICS_SELECT", "select-invalid", { type: 9 }); assert.equal(invalid.kind, "ERROR"); assert.equal(invalid.payload.gate_reason, "xyzac_kinematics_type_invalid"); assert.equal(send("PROGRAM_LOAD", "program-load", { source: PROGRAM, path: "xyzac-panel.ngc" }).kind, "COMMAND_RESULT"); const clear = send("VISUAL_CLEAR", "visual-clear"); assert.equal(clear.kind, "COMMAND_RESULT"); assert.equal(clear.payload.visual_only, true); assert.deepEqual(clear.payload.before, clear.payload.after); assert.equal(send("PROGRAM_RUN", "program-run").kind, "COMMAND_RESULT"); const runningSwitch = send("KINEMATICS_SELECT", "select-running", { type: 0 }); assert.equal(runningSwitch.kind, "ERROR"); assert.equal(runningSwitch.payload.gate_reason, "xyzac_interp_running"); send("PROGRAM_ABORT", "program-abort"); const sent = []; const fakeClient = { send: async (kind, payload, operationId) => { sent.push({ kind, payload, operationId }); return { kind: "COMMAND_RESULT", payload: { status: "verified" } }; } }; const controller = new AxisOperationController(fakeClient); const disabledState = { task: { state: "READY", mode: "MANUAL", interp_state: "IDLE" }, xyzac: { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true }, gates: { switchkinsXyzac: { action: "switchkinsXyzac", enabled: false, reason: "worker_gate_disabled" } } }; const workerBlocked = await controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: disabledState, action: "switchkinsXyzac" }); assert.equal(workerBlocked.response.kind, "COMMAND_RESULT"); assert.equal(sent.length, 1); const enabledState = { ...disabledState, gates: { switchkinsXyzac: { action: "switchkinsXyzac", enabled: true, reason: "xyzac_m428_available" } } }; await controller.execute("KINEMATICS_SELECT", { type: 1 }, { state: enabledState, action: "switchkinsXyzac" }); assert.equal(sent.length, 2); assert.deepEqual(controller.exportTrace().map((item) => item.phase), ["pending", "result", "pending", "result"]); const appSource = await readFile(new URL("../react/src/App.tsx", import.meta.url), "utf8"); const workerClientSource = await readFile(new URL("../react/src/worker-client.ts", import.meta.url), "utf8"); const opfsBridgeSource = await readFile(new URL("./opfs-worker-bridge.mjs", import.meta.url), "utf8"); const ironLaw = await readFile(new URL("../../docs/linuxcnc-axis-web-iron-law.md", import.meta.url), "utf8"); for (const source of [ironLaw, appSource]) assert.ok(source.includes("linuxcnc"), "axis_source_authority"); for (const symbol of ["manual_ok()", "PROGRAM_RUN/PAUSE/RESUME/ABORT", "JOG_START/JOG_STOP", "MDI_EXECUTE", "COOLANT_COMMAND", "blocked_by_abi"]) assert.ok(ironLaw.includes(symbol), `iron_law_${symbol}`); const panelSource = await readFile(new URL("../react/src/XyzacTrtPanel.tsx", import.meta.url), "utf8"); const contractSource = await readFile(new URL("../react/src/xyzac-panel-contract.ts", import.meta.url), "utf8"); const sessionSource = await readFile(new URL("./sim-session.mjs", import.meta.url), "utf8"); const tpSource = await readFile(new URL("./wasm-linuxcnc-tp.mjs", import.meta.url), "utf8"); const combined = `${appSource}\n${panelSource}\n${contractSource}`; for (const symbol of ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacLiveParameters", "XyzacDiagnostics", "Spindle", "Feed actual", "TP velocity", "Live parameters", "KINEMATICS_SELECT", "VISUAL_CLEAR", "POWER", "switchkinsXyzac", "Operation trace", "Alarms / recovery"]) assert.ok(combined.includes(symbol), symbol); for (const symbol of ["wasmSnapshotReady", "manualReady", "sourceJogScale", "sourceVelocityScale", "spindleOverrideSource", "reopenDefaults", "gates.reset.enabled", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "WASM PENDING", "LinuxCNC WASM override-limits command unavailable"]) assert.ok(appSource.includes(symbol), symbol); for (const symbol of ["loadUiState", "saveUiState", "UI_STATE_SAVE", "UI_STATE_LOAD"]) assert.ok(`${appSource}\n${workerClientSource}\n${opfsBridgeSource}`.includes(symbol), symbol); for (const forbidden of [/defaultValue=\"50\"/, /defaultValue=\"100\"/, /setBlockDelete/, /setOptionalStop/, /state\.status === \"running\" \? \"READING\"/]) assert.equal(forbidden.test(appSource), false, String(forbidden)); for (const forbidden of [/_sim_/, /_malloc/, /\bHEAP(?:U|F|I)?\d*\b/, /navigator\.storage/, /localStorage/, /postMessage\s*\(/]) assert.equal(forbidden.test(combined), false, String(forbidden)); assert.equal(workerClientSource.includes("localStorage"), false); assert.ok(appSource.includes("SELECTED_XYZAC_CASE")); assert.equal(appSource.includes("3axis-tutorial"), false); assert.ok(sessionSource.includes("this.wasmMotion.jog(")); assert.equal(/const\s+target\s*=\s*Number\(start\[axis\]/.test(sessionSource), false); assert.equal(/wasmMotion\.enqueue\(\{\s*start,\s*end/.test(sessionSource), false); assert.equal(tpSource.includes("cubicInterpolate"), false); assert.ok(tpSource.includes("sim_linuxcnc_tp_jog")); console.log(JSON.stringify({ schema: "xyzac-protocol-react-rtcp-09.v1", status: "verified", case: CASE, commands: [power.payload.command, "KINEMATICS_SELECT", "VISUAL_CLEAR"], switchkins: switches, gates: { invalid: invalid.payload.gate_reason, running: runningSwitch.payload.gate_reason, controller: controller.exportTrace().map((item) => item.phase) }, components: ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacDiagnostics"], worker_only: true, state_hash: clear.payload.panel_snapshot.state_hash }));