98 lines
7.9 KiB
JavaScript
98 lines
7.9 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);
|
|
const run = send("PROGRAM_RUN", "program-run");
|
|
assert.equal(run.kind, "COMMAND_RESULT");
|
|
assert.equal(Object.hasOwn(run.payload.panel_snapshot.visual, "toolpath"), false);
|
|
assert.equal(run.payload.xyzac_ledger.entries.some((entry) => Object.hasOwn(entry, "response")), false);
|
|
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", "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 }));
|