Make machine configuration authoritative and restore native checkpoints exactly
This commit is contained in:
59
scripts/audit_configuration_inputs.py
Normal file
59
scripts/audit_configuration_inputs.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit the configuration input migration, without classifying every literal as an error."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
sources = {
|
||||
"machine_catalog": "web/workspace/machine-catalog.json",
|
||||
"resource_catalog": "web/workspace/simulation-resources.json",
|
||||
"web_settings": "web/workspace/web-settings.json",
|
||||
"ini_reader": "web/workspace/trt-configuration.mjs",
|
||||
"worker": "web/worker/sim-session.mjs",
|
||||
"tp_adapter": "web/worker/wasm-linuxcnc-tp.mjs",
|
||||
"tp_core": "wasm-core/src/sim_linuxcnc_tp.c",
|
||||
"kinematics_adapter": "web/worker/wasm-xyzac-kinematics.mjs",
|
||||
"case_loader": "web/workspace/sim-case-loader.mjs",
|
||||
"model": "web/react/src/xyzac-machine-model.ts",
|
||||
"machine_view": "web/react/src/XyzacMachineView.tsx",
|
||||
}
|
||||
content = {key: (ROOT / path).read_text() for key, path in sources.items()}
|
||||
checks = {
|
||||
"machine_catalog_parses": bool(json.loads(content["machine_catalog"])["machines"]),
|
||||
"resource_catalog_parses": bool(json.loads(content["resource_catalog"])["runtime_fixtures"]),
|
||||
"settings_parse": bool(json.loads(content["web_settings"])["viewer"]),
|
||||
"worker_reparses_ini": "...readTrtConfiguration(configuration)" in content["worker"],
|
||||
"axis_limits_from_ini": 'limits(`AXIS_${axis.toUpperCase()}`)' in content["ini_reader"],
|
||||
"joint_limits_from_ini": 'limits(`JOINT_${joint}`)' in content["ini_reader"],
|
||||
"no_kinematics_example_limits": "DEFAULT_LIMITS" not in content["kinematics_adapter"],
|
||||
"no_loader_fixed_timing": "servo_cycle_time: 0.001" not in content["case_loader"],
|
||||
"no_loader_axis3_override": "[-240, 100]" not in content["case_loader"],
|
||||
"native_axis_dynamics_input": "sim_linuxcnc_tp_set_axis_dynamics" in content["tp_core"] and "sim_linuxcnc_tp_set_axis_dynamics" in content["tp_adapter"],
|
||||
"no_native_xyzac_speed_override": "fmin(20.0, velocity_limit)" not in content["tp_core"] and "fmin(30.0, velocity_limit)" not in content["tp_core"],
|
||||
"native_acceleration_upper_bound": "*acceleration = fmin" in content["tp_core"] and "*acceleration = fmax" not in content["tp_core"],
|
||||
"model_uses_visual_configuration": "WEB_SETTINGS.viewer.models" in content["model"] and "WEB_SETTINGS.viewer.materials" in content["model"],
|
||||
"machine_path_color_configured": "VIEWER.colors.path" in content["machine_view"],
|
||||
}
|
||||
report = {
|
||||
"report_id": "configuration-input-audit-v1",
|
||||
"status": "verified" if all(checks.values()) else "failed",
|
||||
"scope": "configurable inputs; not a zero-literals claim",
|
||||
"checks": checks,
|
||||
"sources": [{"path": path, "sha256": hashlib.sha256((ROOT / path).read_bytes()).hexdigest()} for path in sources.values()],
|
||||
"wasm_sha256": hashlib.sha256((ROOT / "build-wasm/linuxcnc-sim-core.wasm").read_bytes()).hexdigest(),
|
||||
"retained": ["mathematical constants and tolerances", "ABI layouts, enums, and capacities", "source model mesh topology", "CSS/icon assets", "test fixtures and historical evidence", "upstream LinuxCNC and dependency locks"],
|
||||
}
|
||||
output = ROOT / "coverage/configuration/input-audit.report.json"
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps({"report_id": report["report_id"], "status": report["status"], "checks": len(checks)}))
|
||||
if report["status"] != "verified":
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
scripts/generate_recovery_acceptance.mjs
Normal file
34
scripts/generate_recovery_acceptance.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import createModule from "../build-wasm/linuxcnc-sim-core.js";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
const names = ["axis3-selected-case.report.json", "axis-gates.report.json", "axis3-checkpoint.report.json", "checkpoint-state.report.json", "exact-checkpoint.report.json", "power-off.report.json", "storage-bridge.report.json", "browser-reload.report.json", "browser-reload-xyzbc-trt.report.json"];
|
||||
const evidence = [];
|
||||
for (const name of names) {
|
||||
const path = `coverage/recovery/${name}`;
|
||||
const bytes = await readFile(new URL(path, root));
|
||||
const report = JSON.parse(bytes);
|
||||
assert.equal(report.status, "verified", path);
|
||||
evidence.push({ path, sha256: createHash("sha256").update(bytes).digest("hex") });
|
||||
}
|
||||
const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root));
|
||||
const core = await createModule({ wasmBinary });
|
||||
const pointer = core._sim_checkpoint_build_id();
|
||||
const buildId = new TextDecoder().decode(core.HEAPU8.subarray(pointer, pointer + 64));
|
||||
assert.match(buildId, /^[a-f0-9]{64}$/);
|
||||
const exact = JSON.parse(await readFile(new URL("coverage/recovery/exact-checkpoint.report.json", root)));
|
||||
const report = {
|
||||
report_id: "cfg-r01-trt-r01-acceptance-v1", status: "verified",
|
||||
tasks: ["CFG-R01", "TRT-R01"],
|
||||
native_build_id: buildId,
|
||||
wasm_sha256: createHash("sha256").update(wasmBinary).digest("hex"),
|
||||
checkpoint_sizes: Object.fromEntries(["sim_core", "sim_protocol", "sim_event", "sim_program", "sim_linuxcnc_tp", "sim_hal"].map((name) => [name, core[`_${name}_checkpoint_size`]() ])),
|
||||
continuation_samples: exact.rows.filter((row) => row.phase !== "post-recovery-jog").reduce((sum, row) => sum + (row.samples ?? 0), 0),
|
||||
first_divergent_tick: null,
|
||||
boundaries: ["matching native build and machine configuration required", "old snapshots lacking native state are rejected", "TRT checkpoint event tails require explicit subsequent commands", "backward public recovery requires a fresh transport session", "OPFS retains its existing quota; no physical crash or cross-browser parity claim"],
|
||||
evidence,
|
||||
};
|
||||
await writeFile(new URL("coverage/recovery/acceptance.report.json", root), JSON.stringify(report, null, 2) + "\n");
|
||||
console.log(JSON.stringify({ report_id: report.report_id, status: report.status, continuation_samples: report.continuation_samples, evidence: evidence.length, native_build_id: buildId }));
|
||||
75
scripts/test_checkpoint_reload_browser.mjs
Normal file
75
scripts/test_checkpoint_reload_browser.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
|
||||
const output = new URL("../coverage/recovery/", import.meta.url);
|
||||
await mkdir(output, { recursive: true });
|
||||
const browser = await chromium.launch({ executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
const selectedMachine = process.env.RECOVERY_MACHINE ?? "xyzac-trt";
|
||||
const suffix = selectedMachine === "xyzac-trt" ? "" : `-${selectedMachine}`;
|
||||
if (selectedMachine !== "xyzac-trt") {
|
||||
const catalog = JSON.parse(await readFile(new URL("../web/workspace/machine-catalog.json", import.meta.url), "utf8"));
|
||||
catalog.default_machine = selectedMachine;
|
||||
await page.route(/machine-catalog\.json\?import/, (route) => route.fulfill({ contentType: "text/javascript", body: `export default ${JSON.stringify(catalog)};` }));
|
||||
}
|
||||
const errors = []; page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript(() => {
|
||||
window.recoveryMessages = [];
|
||||
const NativeWorker = window.Worker;
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.addEventListener("message", ({ data }) => {
|
||||
if (!data.payload?.command) return;
|
||||
window.recoveryMessages.push({ command: data.payload.command, kind: data.kind, status: data.payload.status, persistence: data.payload.recovery_persistence, recovery: data.payload.power_off_recovery?.status });
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
||||
const ready = () => page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||
const joints = () => page.locator(".xyzac-machine-view").evaluate((node) => JSON.parse(node.dataset.xyzacJoints));
|
||||
const report = { report_id: "checkpoint-browser-reload-v1", machine: selectedMachine, status: "running" };
|
||||
try {
|
||||
await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/"); await ready();
|
||||
await control("program-file-input").setInputFiles({ name: "recovery-browser.ngc", mimeType: "text/plain", buffer: Buffer.from("G21 G90\nG1 X8 Y3 Z2 F600\nM2\n") });
|
||||
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramPath?.endsWith("recovery-browser.ngc") && document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||
await control("toolbar-power").click(); await control("manual-home-all").click();
|
||||
await page.waitForFunction(() => !document.querySelector('[data-axis-control="manual-axis-x"]').disabled);
|
||||
await control("manual-axis-x").check();
|
||||
const button = await control("manual-jog-positive").boundingBox();
|
||||
await page.mouse.move(button.x + button.width / 2, button.y + button.height / 2); await page.mouse.down();
|
||||
await page.waitForFunction(() => JSON.parse(document.querySelector(".xyzac-machine-view").dataset.xyzacJoints)[0] > 2);
|
||||
await page.mouse.up();
|
||||
await page.waitForFunction(() => window.recoveryMessages.some((row) => row.command === "JOG_STOP"));
|
||||
report.before = await joints();
|
||||
await control("toolbar-power").click();
|
||||
await page.waitForFunction(() => window.recoveryMessages.some((row) => row.command === "POWER" && row.persistence?.status === "verified" && row.persistence?.bytes > 0));
|
||||
report.saved = await page.evaluate(() => window.recoveryMessages.filter((row) => row.command === "POWER" && row.persistence?.bytes > 0).at(-1).persistence);
|
||||
await page.reload(); await ready();
|
||||
await page.waitForFunction(() => window.recoveryMessages.some((row) => row.command === "LOAD_WORKSPACE" && row.recovery === "verified"));
|
||||
report.after = await joints();
|
||||
report.preview_segments = Number(await page.locator(".xyzac-tool-preview").getAttribute("data-xyzac-toolpath-count"));
|
||||
assert.ok(report.preview_segments > 0, "restore must publish the regenerated preview");
|
||||
assert.deepEqual(report.after, report.before);
|
||||
assert.match(await page.locator("main").getAttribute("data-axis-program-path"), /recovery-browser\.ngc$/);
|
||||
assert.equal(await page.evaluate(() => window.recoveryMessages.filter((row) => row.command === "PROGRAM_LOAD").length), 0, "startup must not overwrite the recovered program with the packaged demo");
|
||||
await control("toolbar-power").click();
|
||||
await control("manual-jog-increment").selectOption("0.1000");
|
||||
await control("manual-axis-x").check(); await control("manual-jog-positive").click();
|
||||
await page.waitForFunction((start) => JSON.parse(document.querySelector(".xyzac-machine-view").dataset.xyzacJoints)[0] >= start + .0999, report.after[0]);
|
||||
report.continued = await joints();
|
||||
assert.deepEqual(errors, []); report.status = "verified";
|
||||
await page.screenshot({ path: new URL(`browser-reload${suffix}.png`, output).pathname, fullPage: true });
|
||||
} catch (error) {
|
||||
report.status = "failed"; report.error = String(error.stack ?? error);
|
||||
report.messages = await page.evaluate(() => window.recoveryMessages);
|
||||
await page.screenshot({ path: new URL(`browser-reload-failure${suffix}.png`, output).pathname, fullPage: true });
|
||||
throw error;
|
||||
} finally {
|
||||
report.browser_errors = errors;
|
||||
await writeFile(new URL(`browser-reload${suffix}.report.json`, output), JSON.stringify(report, null, 2) + "\n");
|
||||
await browser.close();
|
||||
}
|
||||
console.log(JSON.stringify(report));
|
||||
65
scripts/test_configurable_ui_browser.mjs
Normal file
65
scripts/test_configurable_ui_browser.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
|
||||
const root = new URL("../", import.meta.url);
|
||||
const settings = JSON.parse(await readFile(new URL("web/workspace/web-settings.json", root), "utf8"));
|
||||
const catalog = JSON.parse(await readFile(new URL("web/workspace/machine-catalog.json", root), "utf8"));
|
||||
settings.ui.jog_increments = ["0.25", "2.5"];
|
||||
settings.viewer.colors.background = "#550055";
|
||||
settings.viewer.colors.path = "#00ff00";
|
||||
settings.assets.axis_images = "configured-icons";
|
||||
catalog.default_machine = "xyzbc-trt";
|
||||
const sourceIni = catalog.machines[1].entrypoint;
|
||||
catalog.machines[1].entrypoint = sourceIni.replace(/[^/]+$/, "configured-machine.ini");
|
||||
catalog.machines[1].program = catalog.machines[1].program.replace(/[^/]+$/, "configured-preview.ngc");
|
||||
const browser = await chromium.launch({ executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
const errors = []; page.on("pageerror", (error) => errors.push(error.message));
|
||||
const configRequests = [], iconRequests = [];
|
||||
await page.route(/web-settings\.json\?import|machine-catalog\.json\?import/, async (route) => {
|
||||
configRequests.push(route.request().url());
|
||||
const value = route.request().url().includes("web-settings.json") ? settings : catalog;
|
||||
await route.fulfill({ contentType: "text/javascript", body: `export default ${JSON.stringify(value)};` });
|
||||
});
|
||||
await page.route("**/configured-icons/*", async (route) => {
|
||||
iconRequests.push(route.request().url());
|
||||
const name = new URL(route.request().url()).pathname.split("/").at(-1);
|
||||
const response = await route.fetch({ url: new URL(`/linuxcnc/share/axis/images/${name}`, route.request().url()).href });
|
||||
await route.fulfill({ response });
|
||||
});
|
||||
await page.route("**/configured-machine.ini", async (route) => {
|
||||
const response = await route.fetch({ url: new URL(`/${sourceIni}`, route.request().url()).href });
|
||||
const content = (await response.text()).replace(/MAX_FEED_OVERRIDE\s*=\s*[^\r\n]+/, "MAX_FEED_OVERRIDE = 1.3");
|
||||
await route.fulfill({ response, body: content });
|
||||
});
|
||||
await page.route("**/configured-preview.ngc", (route) => route.fulfill({ contentType: "text/plain", body: "G21 G90\nG0 X-30 Y-30 Z20\nG1 X30 Y-30 F600\nY30\nX-30\nY-30\nM2\n" }));
|
||||
try {
|
||||
await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/");
|
||||
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||
assert.equal(await page.locator(".xyzac-machine-view").getAttribute("data-xyzac-model-id"), "xyzbc-trt");
|
||||
assert.equal(await page.locator("main").getAttribute("data-axis-program-path"), catalog.machines[1].program);
|
||||
assert.deepEqual(await page.locator('[data-axis-control="manual-jog-increment"] option').allTextContents(), ["Continuous", "0.25", "2.5"]);
|
||||
assert.equal(await page.locator('[data-axis-control="override-feed"]').getAttribute("max"), "130");
|
||||
assert.ok(configRequests.length >= 2); assert.ok(iconRequests.length > 10);
|
||||
const colors = await page.locator('.xyzac-machine-view canvas[data-engine]').evaluate(async (canvas) => {
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
const gl = canvas.getContext("webgl2"); const data = new Uint8Array(canvas.width * canvas.height * 4);
|
||||
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
let purple = 0, green = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] > 30 && data[i + 2] > 30 && data[i + 1] < 10) purple++;
|
||||
if (data[i + 1] > 150 && data[i + 1] > data[i] + 30 && data[i + 1] > data[i + 2] + 30) green++;
|
||||
}
|
||||
return { purple, green };
|
||||
});
|
||||
assert.ok(colors.purple > 1000, JSON.stringify(colors));
|
||||
await page.screenshot({ path: new URL("coverage/configuration/configured-ui.png", root).pathname, fullPage: true });
|
||||
assert.ok(colors.green > 0, JSON.stringify(colors));
|
||||
assert.deepEqual(errors, []);
|
||||
const output = new URL("coverage/configuration/", root); await mkdir(output, { recursive: true });
|
||||
await page.screenshot({ path: new URL("configured-ui.png", output).pathname, fullPage: true });
|
||||
const report = { report_id: "configurable-ui-browser-v1", status: "verified", default_machine: catalog.default_machine, entrypoint: catalog.machines[1].entrypoint, program: catalog.machines[1].program, increments: settings.ui.jog_increments, feed_max: 1.3, colors, configured_icons: iconRequests.length, browser_errors: errors };
|
||||
await writeFile(new URL("browser.report.json", output), JSON.stringify(report, null, 2) + "\n");
|
||||
console.log(JSON.stringify(report));
|
||||
} finally { await browser.close(); }
|
||||
Reference in New Issue
Block a user