Files
linuxcncWebWASM/web/worker/test_xyzac_protocol_react.mjs

95 lines
7.8 KiB
JavaScript

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 }));