Add OPFS G-code staging and browser simulation checks
This commit is contained in:
@@ -34,6 +34,16 @@
|
||||
}
|
||||
return win.webRtcp5AxisSimulation.getState();
|
||||
}
|
||||
async function waitForMachineFileStaging(win) {
|
||||
for (let attempt = 0; attempt < 120; attempt += 1) {
|
||||
const state = win.webRtcp5AxisSimulation.getState();
|
||||
if (state.machineFileStaging?.status === "staged" && state.machineFileStaging?.save?.fileCount >= 5) {
|
||||
return state;
|
||||
}
|
||||
await wait(25);
|
||||
}
|
||||
return win.webRtcp5AxisSimulation.getState();
|
||||
}
|
||||
|
||||
async function runSmoke() {
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -44,6 +54,14 @@
|
||||
|
||||
const doc = frame.contentDocument;
|
||||
const win = frame.contentWindow;
|
||||
const runtimeErrors = [];
|
||||
win.addEventListener("error", (event) => {
|
||||
runtimeErrors.push(event.message || String(event.error || "window error"));
|
||||
});
|
||||
win.addEventListener("unhandledrejection", (event) => {
|
||||
runtimeErrors.push(event.reason?.message || String(event.reason || "unhandled rejection"));
|
||||
});
|
||||
win.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__ = true;
|
||||
const regions = [
|
||||
"titlebar",
|
||||
"preview",
|
||||
@@ -301,6 +319,20 @@
|
||||
if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) {
|
||||
throw new Error("program switchkins summary DOM did not render zero-event state");
|
||||
}
|
||||
if (!win.webRtcp5AxisSimulation.machineFileSeedReady) {
|
||||
throw new Error("missing automatic machine file seed promise");
|
||||
}
|
||||
await win.webRtcp5AxisSimulation.machineFileSeedReady;
|
||||
const autoStagedState = await waitForMachineFileStaging(win);
|
||||
if (autoStagedState.machineFileStaging?.save?.summary?.gcodeFileCount !== 16) {
|
||||
throw new Error(`automatic machine file seeding did not preload full project G-code set: ${JSON.stringify(autoStagedState.machineFileStaging?.save?.summary)}`);
|
||||
}
|
||||
if (autoStagedState.machineFileStaging?.gcodeSources?.length < 8) {
|
||||
throw new Error(`automatic machine file seeding did not expose LinuxCNC 5-axis demos: ${JSON.stringify(autoStagedState.machineFileStaging)}`);
|
||||
}
|
||||
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
|
||||
throw new Error("automatic machine file staging status did not render");
|
||||
}
|
||||
win.webRtcp5AxisSimulation.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-arc-demo.ngc",
|
||||
@@ -328,12 +360,27 @@
|
||||
if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) {
|
||||
throw new Error(`machine file staging did not save files: ${JSON.stringify(stagedMachineFiles.save)}`);
|
||||
}
|
||||
if (stagedMachineFiles.save.summary?.gcodeFileCount !== 16) {
|
||||
throw new Error(`manual machine file staging did not preserve full project G-code set: ${JSON.stringify(stagedMachineFiles.save.summary)}`);
|
||||
}
|
||||
if (
|
||||
stagedMachineFiles.save.storageMode !== "memory-fallback" ||
|
||||
stagedMachineFiles.save.storageCapability?.opfsUnavailable !== true
|
||||
) {
|
||||
throw new Error(`machine file staging did not use OPFS fallback: ${JSON.stringify(stagedMachineFiles.save.storageCapability)}`);
|
||||
}
|
||||
if (!stagedMachineFiles.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc"))) {
|
||||
throw new Error("machine file staging did not include M428 remap");
|
||||
}
|
||||
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
|
||||
throw new Error("machine file staging status did not render");
|
||||
}
|
||||
if (
|
||||
!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("memory-fallback") ||
|
||||
!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("forced_unavailable")
|
||||
) {
|
||||
throw new Error("machine file staging status did not render OPFS fallback reason");
|
||||
}
|
||||
const sourceSelect = doc.querySelector('[data-action="select-linuxcnc-gcode-source"]');
|
||||
if (!sourceSelect || sourceSelect.options.length < 4) {
|
||||
throw new Error("LinuxCNC 5-axis G-code source selector did not render staged demos");
|
||||
@@ -355,6 +402,45 @@
|
||||
if (!doc.querySelector('[data-program-source]')?.textContent.includes("linuxcnc-vendored-5axis-gcode")) {
|
||||
throw new Error("program source DOM did not show LinuxCNC vendored source");
|
||||
}
|
||||
if (linuxCncSourceState.programExecution?.summary?.motionEventCount < 1000) {
|
||||
throw new Error(`LinuxCNC source program did not produce canonical motion: ${JSON.stringify(linuxCncSourceState.programExecution?.summary)}`);
|
||||
}
|
||||
if (linuxCncSourceState.programExecution?.summary?.plannerRuntimeReady !== true) {
|
||||
throw new Error(`LinuxCNC source program did not produce TP timing: ${JSON.stringify(linuxCncSourceState.programExecution?.plannerTiming)}`);
|
||||
}
|
||||
if (linuxCncSourceState.programExecutionTiming?.samples?.length < 1) {
|
||||
throw new Error("LinuxCNC source program did not expose TP samples");
|
||||
}
|
||||
if (linuxCncSourceState.programExecution?.summary?.switchkinsEventCount < 2) {
|
||||
throw new Error("LinuxCNC source program did not preserve switchkins events");
|
||||
}
|
||||
if (linuxCncSourceState.rtcpState !== "on" || linuxCncSourceState.kinsType !== "tcp-xyzac") {
|
||||
throw new Error(`LinuxCNC source program did not apply initial RTCP switchkins state: ${linuxCncSourceState.rtcpState}/${linuxCncSourceState.kinsType}`);
|
||||
}
|
||||
canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||
if (
|
||||
canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" ||
|
||||
Number(canvas.dataset.threePathPoints ?? 0) < 1 ||
|
||||
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 ||
|
||||
canvas.dataset.threeToolExecutionMarker !== "true"
|
||||
) {
|
||||
throw new Error(`Three.js preview did not expose LinuxCNC source program path and execution trace: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
if (
|
||||
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
||||
canvas.dataset.threeToolpathPreviewSource !== "linuxcnc_interpreter_canonical_motion" ||
|
||||
canvas.dataset.threeToolExecutionTraceSource !== "linuxcnc_tp_samples_or_task_motion_hal_feedback" ||
|
||||
canvas.dataset.threePathFitBounds !== "ok" ||
|
||||
canvas.dataset.threeCurrentSegmentHighlight !== "ok" ||
|
||||
canvas.dataset.threeRapidFeedVisualDistinction !== "ok" ||
|
||||
canvas.dataset.threeNoGcodeSemanticsGeneration !== "ok"
|
||||
) {
|
||||
throw new Error(`Three.js M20 source/runtime display gate failed: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
if (Number(canvas.dataset.threeRapidPathPoints ?? 0) < 1 || Number(canvas.dataset.threeFeedPathPoints ?? 0) < 1) {
|
||||
throw new Error(`Three.js rapid/feed distinction did not expose path points: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
assertCanvasNonblank(canvas, "desktop LinuxCNC source Three.js preview");
|
||||
win.webRtcp5AxisSimulation.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||
const machineFileRunState = await waitForMachineFileExecution(win);
|
||||
if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) {
|
||||
@@ -383,12 +469,18 @@
|
||||
if (!["opfs", "memory-fallback"].includes(savedSession.storageMode)) {
|
||||
throw new Error(`saveSession used unexpected storage mode: ${savedSession.storageMode}`);
|
||||
}
|
||||
if (savedSession.storageMode !== "memory-fallback" || savedSession.storageCapability?.opfsUnavailable !== true) {
|
||||
throw new Error(`saveSession did not use OPFS fallback: ${JSON.stringify(savedSession.storageCapability)}`);
|
||||
}
|
||||
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("saved")) {
|
||||
throw new Error("session save status did not render");
|
||||
}
|
||||
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes(savedSession.storageMode)) {
|
||||
throw new Error("session save status did not render storage mode");
|
||||
}
|
||||
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("opfs unavailable")) {
|
||||
throw new Error("session save status did not render OPFS unavailable fallback");
|
||||
}
|
||||
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 2") {
|
||||
throw new Error("loaded program did not render first LinuxCNC motion line");
|
||||
}
|
||||
@@ -413,10 +505,22 @@
|
||||
canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||
if (
|
||||
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 ||
|
||||
canvas.dataset.threeToolExecutionMarker !== "true"
|
||||
canvas.dataset.threeToolExecutionMarker !== "true" ||
|
||||
canvas.dataset.threeCurrentSegmentHighlight !== "ok"
|
||||
) {
|
||||
throw new Error(`Three.js preview did not display tool execution progress: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
const originalFrameStyle = frame.getAttribute("style") || "";
|
||||
frame.style.width = "390px";
|
||||
frame.style.height = "760px";
|
||||
await wait(100);
|
||||
canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||
if (canvas.dataset.threeReady !== "true" || canvas.dataset.threePathFitBounds !== "ok") {
|
||||
throw new Error(`mobile Three.js preview did not retain fit bounds: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
assertCanvasNonblank(canvas, "mobile LinuxCNC source Three.js preview");
|
||||
frame.setAttribute("style", originalFrameStyle);
|
||||
await wait(50);
|
||||
const taskHalRunState = win.webRtcp5AxisSimulation.getState();
|
||||
if (taskHalRunState.fullExecutionBoundary?.semanticBoundary !== "linuxcnc_task_motion_hal_wasm_simulation_runtime") {
|
||||
throw new Error(`full execution boundary did not promote task/HAL simulation runtime: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`);
|
||||
@@ -427,6 +531,27 @@
|
||||
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full ready")) {
|
||||
throw new Error("full execution boundary status did not render full ready state");
|
||||
}
|
||||
const hostNativeBoundary = doc.querySelector('[data-full-execution-boundary="host-native"]')?.textContent || "";
|
||||
for (const requiredHostNativeState of [
|
||||
"hardware drive false",
|
||||
"host realtime false",
|
||||
"external user-M false",
|
||||
"tool DB false",
|
||||
]) {
|
||||
if (!hostNativeBoundary.includes(requiredHostNativeState)) {
|
||||
throw new Error(`host/native boundary diagnostic did not render ${requiredHostNativeState}: ${hostNativeBoundary}`);
|
||||
}
|
||||
}
|
||||
for (const [field, expected] of Object.entries({
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
externalUserMProcessReady: false,
|
||||
toolDbProcessReady: false,
|
||||
})) {
|
||||
if (taskHalRunState.fullExecutionBoundary?.[field] !== expected) {
|
||||
throw new Error(`full execution boundary ${field} drifted: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`);
|
||||
}
|
||||
}
|
||||
if (!doc.querySelector('[data-task-hal-runtime="readiness"]')?.textContent.includes("sync")) {
|
||||
throw new Error("task/HAL readiness diagnostic did not render sync");
|
||||
}
|
||||
@@ -545,6 +670,9 @@
|
||||
if (uiRestoredSession.status !== "restored" || uiRestoredSession.storageMode !== uiSavedSession.storageMode) {
|
||||
throw new Error(`UI Restore Session did not restore from default storage fallback: ${JSON.stringify(uiRestoredSession)}`);
|
||||
}
|
||||
if (uiRestoredSession.storageMode !== "memory-fallback") {
|
||||
throw new Error(`UI Restore Session did not preserve memory fallback mode: ${JSON.stringify(uiRestoredSession)}`);
|
||||
}
|
||||
if (win.webRtcp5AxisSimulation.getState().machine.allHomed !== true) {
|
||||
doc.querySelector('[data-action="mode-manual"]').click();
|
||||
await wait(50);
|
||||
@@ -673,6 +801,9 @@
|
||||
if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") {
|
||||
throw new Error("E-STOP did not update run state");
|
||||
}
|
||||
if (runtimeErrors.length > 0) {
|
||||
throw new Error(`public HTTP fallback smoke saw uncaught runtime errors: ${runtimeErrors.join(" | ")}`);
|
||||
}
|
||||
|
||||
result.textContent = "gmoccapy_shell_smoke=ok";
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ assert.equal(plan.files.some((file) => file.sourceRel.endsWith("xyzac-trt.tbl"))
|
||||
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc")), true);
|
||||
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/429remap.ngc")), true);
|
||||
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("demos/xyzac_switchkins.ngc")), true);
|
||||
assert.equal(plan.summary.gcodeFileCount, 16);
|
||||
assert.equal(plan.summary.remapFileCount >= 3, true);
|
||||
assert.equal(plan.summary.demoFileCount >= 1, true);
|
||||
|
||||
@@ -40,10 +41,14 @@ const staged = await stageProfileMachineFiles(profile, { storage });
|
||||
assert.equal(staged.save.apiName, "web-rtcp-5axis-machine-file-staging-save");
|
||||
assert.equal(staged.save.status, "saved");
|
||||
assert.equal(staged.save.fileCount, staged.plan.files.length);
|
||||
assert.equal(staged.save.semanticBoundary, "opfs_machine_file_text_staging_only");
|
||||
assert.equal(staged.save.storageMode, "memory");
|
||||
assert.equal(staged.save.semanticBoundary, "memory_machine_file_text_staging_current_page_lifecycle_only");
|
||||
assert.equal(staged.save.summary.gcodeFileCount, 16);
|
||||
assert.equal(staged.save.gcodeFiles.length, 16);
|
||||
assert.equal(staged.save.summary.kinds.remap >= 3, true);
|
||||
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "impeller-7bl-xyzac.ngc"), true);
|
||||
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "boat-xyzac.ngc"), true);
|
||||
assert.equal(staged.save.gcodeFiles.some((file) => file.sourceRel.endsWith("remap_subs/430remap.ngc")), true);
|
||||
assert.equal(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-sim-plan/machines/xyzac-trt/")), true);
|
||||
assert.throws(
|
||||
() => selectMachineFileProgram(staged.plan, staged.save, "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc"),
|
||||
@@ -64,6 +69,9 @@ assert.equal(storeStage.save.fileCount, staged.save.fileCount);
|
||||
assert.equal(state.machineFileStaging.status, "staged");
|
||||
assert.equal(state.machineFileStaging.profileId, "xyzac-trt");
|
||||
assert.equal(state.machineFileStaging.fileCount, staged.save.fileCount);
|
||||
assert.equal(state.machineFileStaging.storageMode, "memory");
|
||||
assert.equal(state.machineFileStaging.storageCapability.reason, "explicit_storage");
|
||||
assert.equal(state.machineFileStaging.save.summary.gcodeFileCount, 16);
|
||||
assert.equal(state.machineFileStaging.save.summary.kinds.remap >= 3, true);
|
||||
assert.equal(state.machineFileStaging.gcodeSources.length >= 4, true);
|
||||
assert.equal(state.machineFileStaging.selectedGcodeSourceRel, null);
|
||||
@@ -125,4 +133,16 @@ assert.equal(runState.fullExecutionBoundary.satisfied.includes("fiveaxis-remap-m
|
||||
assert.equal(runState.fullExecutionBoundary.satisfied.includes("switchkins-hal-bridge-evidence"), true);
|
||||
assert.equal(runState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner")), true);
|
||||
|
||||
globalThis.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__ = true;
|
||||
const fallbackStore = createSimulationStore();
|
||||
const fallbackStage = await fallbackStore.stageMachineFiles();
|
||||
const fallbackState = fallbackStore.getState();
|
||||
assert.equal(fallbackStage.save.storageMode, "memory-fallback");
|
||||
assert.equal(fallbackStage.save.storageCapability.opfsUnavailable, true);
|
||||
assert.equal(fallbackStage.save.storageCapability.reason, "forced_unavailable");
|
||||
assert.equal(fallbackState.machineFileStaging.status, "staged");
|
||||
assert.equal(fallbackState.machineFileStaging.storageMode, "memory-fallback");
|
||||
assert.equal(fallbackState.operatorMessage.includes("staged"), true);
|
||||
delete globalThis.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__;
|
||||
|
||||
console.log("machine_file_staging_smoke=ok");
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
import { createFullLinuxCncExecutionBoundary } from "../../app/src/runtime/full-execution-boundary.js";
|
||||
import { createNativeTaskHalReadinessAudit } from "../../app/src/runtime/native-task-hal-audit.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const projectDir = resolve(__dirname, "../..");
|
||||
const rootDir = resolve(projectDir, "..");
|
||||
const phase0Script = resolve(rootDir, "wasm-port/tests/native/verify_task_hal_phase0.sh");
|
||||
const manifestLog = resolve(rootDir, "wasm-port/build/task-hal/verify_task_hal_source_manifest.stdout.log");
|
||||
const probeLog = resolve(rootDir, "wasm-port/build/task-hal/probe_trt_task_hal_runtime.stdout.log");
|
||||
const manifestReport = resolve(rootDir, "wasm-port/build/task-hal/task-hal-source-manifest.tsv");
|
||||
const artifactDir = resolve(projectDir, "build/readiness");
|
||||
const artifactPath = resolve(artifactDir, "native-task-hal-readiness.json");
|
||||
|
||||
run(phase0Script);
|
||||
|
||||
const sourceManifest = parseKeyValueFile(manifestLog);
|
||||
const nativeProbe = parseKeyValueFile(probeLog);
|
||||
const fullExecutionBoundary = createFullLinuxCncExecutionBoundary(createPromotedSimulationState());
|
||||
const audit = createNativeTaskHalReadinessAudit({
|
||||
sourceManifest,
|
||||
nativeProbe,
|
||||
fullExecutionBoundary,
|
||||
taskHalRuntimeReadiness: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
},
|
||||
taskHalStatus: {
|
||||
summary: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
taskHalComparisonReady: true,
|
||||
},
|
||||
},
|
||||
generatedAt: "2026-06-22T00:00:00.000Z",
|
||||
artifactPaths: {
|
||||
readinessJson: relativeFromRoot(artifactPath),
|
||||
sourceManifestLog: relativeFromRoot(manifestLog),
|
||||
sourceManifestReport: relativeFromRoot(manifestReport),
|
||||
nativeProbeLog: relativeFromRoot(probeLog),
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(audit.status, "ok");
|
||||
assert.equal(audit.taskHalWebSimulationBoundaryConsistent, true);
|
||||
assert.equal(audit.webSimulation.promoted, true);
|
||||
assert.equal(audit.webSimulation.nativeTaskReady, true);
|
||||
assert.equal(audit.webSimulation.nativeHalSyncReady, true);
|
||||
assert.equal(audit.webSimulation.fullLinuxCncProgramExecutionReady, true);
|
||||
assert.equal(audit.nativeHostAndHardware.nativeProbe, "ok");
|
||||
assert.match(audit.nativeHostAndHardware.nativeProbeStatus, /^(passed|ready_disabled_by_default|skipped_missing_host_runtime)$/);
|
||||
assert.equal(audit.nativeHostAndHardware.nativePromotionAllowed, false);
|
||||
assert.equal(audit.nativeHostAndHardware.hardwareDrive, false);
|
||||
assert.equal(audit.nativeHostAndHardware.hostRealtimeKernel, false);
|
||||
assert.equal(audit.nativeHostAndHardware.externalUserMProcessReady, false);
|
||||
assert.equal(audit.nativeHostAndHardware.toolDbProcessReady, false);
|
||||
assert.equal(audit.sourceManifest.ready, true);
|
||||
assert.equal(audit.sourceManifest.taskSourceCount > 0, true);
|
||||
assert.equal(audit.sourceManifest.halSourceCount > 0, true);
|
||||
assert.equal(audit.sourceManifest.motionSourceCount > 0, true);
|
||||
assert.equal(audit.gates.task_hal_web_simulation_boundary_consistent, 1);
|
||||
assert.equal(audit.gates.hardware_drive, 0);
|
||||
assert.equal(audit.gates.host_realtime_kernel, 0);
|
||||
assert.equal(audit.gates.external_user_m_process_ready, 0);
|
||||
assert.equal(audit.gates.tool_db_process_ready, 0);
|
||||
assert.equal(audit.gates.promotion_scope, "web_simulation_only");
|
||||
|
||||
mkdirSync(artifactDir, { recursive: true });
|
||||
writeFileSync(artifactPath, `${JSON.stringify(audit, null, 2)}\n`);
|
||||
|
||||
const saved = JSON.parse(readFileSync(artifactPath, "utf8"));
|
||||
assert.equal(saved.apiName, "web-rtcp-5axis-native-task-hal-readiness-audit");
|
||||
assert.equal(saved.status, "ok");
|
||||
|
||||
console.log("native_task_hal_source_artifact_audit=ok");
|
||||
console.log(`native_task_hal_readiness_artifact=${relativeFromRoot(artifactPath)}`);
|
||||
console.log("task_hal_web_simulation_boundary_consistent=1");
|
||||
console.log(`native_task_hal_host_probe_status=${audit.nativeHostAndHardware.nativeProbeStatus}`);
|
||||
console.log("hardware_drive=0");
|
||||
console.log("host_realtime_kernel=0");
|
||||
console.log("external_user_m_process_ready=0");
|
||||
console.log("tool_db_process_ready=0");
|
||||
console.log("promotion_scope=web_simulation_only");
|
||||
|
||||
function createPromotedSimulationState() {
|
||||
const programExecution = {
|
||||
sourceMode: "linuxcnc-interpreter-wasm",
|
||||
plannerTiming: {
|
||||
plannerRuntimeReady: true,
|
||||
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
},
|
||||
summary: {
|
||||
motionEventCount: 3,
|
||||
canonicalEventCount: 8,
|
||||
plannerRuntimeReady: true,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
machineProfile: "xyzac-trt",
|
||||
linuxCncBoundaryAdapter: {
|
||||
linuxCncKinematicsReady: true,
|
||||
linuxCncInterpreterReady: true,
|
||||
},
|
||||
rtcpFrame: {
|
||||
semanticBoundary: "linuxcnc_kinematics_wasm_c_abi",
|
||||
readiness: { linuxCncKinematicsReady: true },
|
||||
},
|
||||
interpreterRuntimeReadiness: {
|
||||
loaded: true,
|
||||
semanticBoundary: "linuxcnc_interpreter_wasm_canonical_events",
|
||||
},
|
||||
programExecution,
|
||||
machineFileStaging: {
|
||||
status: "staged",
|
||||
fileCount: 12,
|
||||
},
|
||||
machineFileExecution: {
|
||||
sourceMode: "linuxcnc-machine-file-remap-wasm",
|
||||
summary: { machineFileExecutionReady: true },
|
||||
resultText: [
|
||||
"fiveaxis_ini_open=1",
|
||||
"fiveaxis_remaps_ready=1",
|
||||
"fiveaxis_file_reached_exit=1",
|
||||
"fiveaxis_hal_switchkins: rc=0 found=1 value=1",
|
||||
].join("\n"),
|
||||
},
|
||||
taskHalRuntimeReadiness: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
},
|
||||
taskHalStatus: {
|
||||
summary: {
|
||||
taskRuntimeReady: true,
|
||||
motionRuntimeReady: true,
|
||||
halRuntimeReady: true,
|
||||
halSyncReady: true,
|
||||
taskHalComparisonReady: true,
|
||||
},
|
||||
ui: {
|
||||
taskCycle: 2,
|
||||
servoCycle: 20,
|
||||
motionQueueDepth: 0,
|
||||
halChangedPinCount: 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function run(scriptPath) {
|
||||
const result = spawnSync("bash", [scriptPath], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.stdout.write(result.stdout || "");
|
||||
process.stderr.write(result.stderr || "");
|
||||
throw new Error(`${scriptPath} failed with status ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseKeyValueFile(path) {
|
||||
const fields = {};
|
||||
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const equals = line.indexOf("=");
|
||||
if (equals <= 0) continue;
|
||||
fields[line.slice(0, equals)] = line.slice(equals + 1);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function relativeFromRoot(path) {
|
||||
return path.startsWith(`${rootDir}/`) ? path.slice(rootDir.length + 1) : path;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import {
|
||||
selectMachineFileProgram,
|
||||
stageProfileMachineFiles,
|
||||
} from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const TRT_DEMO_PREFIX = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/";
|
||||
const DIRECT_CANONICAL_CASES = [
|
||||
"boat-xyzac.ngc",
|
||||
"boat-xyzbc.ngc",
|
||||
"impeller-7bl-xyzac.ngc",
|
||||
"xyzac_switchkins_test_1.ngc",
|
||||
"xyzac_switchkins_test_2.ngc",
|
||||
"xyzac_switchkins_test_3.ngc",
|
||||
];
|
||||
const ENTRY_MACHINE_FILE_CASES = [
|
||||
"xyzac_switchkins.ngc",
|
||||
"xyzbc_switchkins.ngc",
|
||||
];
|
||||
|
||||
const runtime = await createLinuxCncInterpreterRuntime();
|
||||
const staged = await stageProfileMachineFiles(getFiveAxisProfile("xyzac-trt"), {
|
||||
storage: createMemorySessionStorage(),
|
||||
});
|
||||
const sourceByFilename = new Map(staged.save.gcodeSources.map((source) => [source.filename, source]));
|
||||
|
||||
for (const filename of [...DIRECT_CANONICAL_CASES, ...ENTRY_MACHINE_FILE_CASES]) {
|
||||
const source = sourceByFilename.get(filename);
|
||||
assert.ok(source, `${filename} must be present in staged LinuxCNC demo sources`);
|
||||
assert.equal(source.sourceRel, `${TRT_DEMO_PREFIX}${filename}`);
|
||||
assert.equal(source.sourceMode, "linuxcnc-vendored-5axis-gcode");
|
||||
assert.equal(source.semanticBoundary, "linuxcnc_vendored_5axis_gcode_source_file");
|
||||
}
|
||||
|
||||
const canonicalReports = [];
|
||||
for (const filename of DIRECT_CANONICAL_CASES) {
|
||||
const source = sourceByFilename.get(filename);
|
||||
const file = staged.save.files.find((candidate) => candidate.sourceRel === source.sourceRel);
|
||||
const execution = runtime.runProgram(file.text);
|
||||
|
||||
assert.equal(execution.sourceMode, "linuxcnc-interpreter-wasm", `${filename} source mode`);
|
||||
assert.equal(execution.semanticBoundary, "linuxcnc_interpreter_wasm_canonical_events", `${filename} boundary`);
|
||||
assert.equal(execution.summary.motionEventCount > 0, true, `${filename} canonical motion`);
|
||||
assert.equal(execution.summary.ready, true, `${filename} ready`);
|
||||
assert.equal(execution.summary.plannerRuntimeReady, true, `${filename} TP timing ready`);
|
||||
assert.equal(execution.plannerTiming.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
|
||||
assert.equal(execution.plannerTiming.motionCount, execution.motion.length, `${filename} TP motion count`);
|
||||
assert.equal(execution.plannerTiming.segments.length, execution.motion.length, `${filename} TP segment count`);
|
||||
assert.equal(execution.plannerTiming.samples.length > 0, true, `${filename} TP samples`);
|
||||
|
||||
const firstMotionLine = execution.motion.find((event) => Number.isFinite(Number(event.line)))?.line;
|
||||
assert.equal(Number.isFinite(Number(firstMotionLine)), true, `${filename} current source line`);
|
||||
assert.equal(execution.motion.every((event) => event.statement !== undefined), true, `${filename} source statements`);
|
||||
|
||||
if (execution.summary.switchkinsEventCount > 0) {
|
||||
assert.equal(
|
||||
execution.switchkinsEvents.some((event) => event.switchkinsType === 1),
|
||||
true,
|
||||
`${filename} switchkins TCP event`,
|
||||
);
|
||||
assert.equal(
|
||||
execution.switchkinsEvents.some((event) => event.switchkinsType === 0),
|
||||
true,
|
||||
`${filename} switchkins identity event`,
|
||||
);
|
||||
assert.equal(
|
||||
execution.summary.switchkinsRemapBoundary,
|
||||
"linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
|
||||
`${filename} switchkins boundary`,
|
||||
);
|
||||
}
|
||||
|
||||
canonicalReports.push({
|
||||
filename,
|
||||
previewPoints: execution.motion.length,
|
||||
executedPathPoints: execution.plannerTiming.samples.length,
|
||||
currentLine: firstMotionLine,
|
||||
switchkinsEvents: execution.summary.switchkinsEventCount,
|
||||
});
|
||||
}
|
||||
|
||||
for (const filename of ENTRY_MACHINE_FILE_CASES) {
|
||||
const source = sourceByFilename.get(filename);
|
||||
const selectedPlan = selectMachineFileProgram(staged.plan, staged.save, source.sourceRel);
|
||||
const execution = runtime.runMachineFileProgram({
|
||||
plan: selectedPlan,
|
||||
files: staged.save.files,
|
||||
executionMode: "fiveAxisRemap",
|
||||
});
|
||||
|
||||
assert.equal(execution.sourceMode, "linuxcnc-machine-file-remap-wasm", `${filename} machine-file source`);
|
||||
assert.equal(execution.summary.machineFileExecutionReady, true, `${filename} machine-file ready`);
|
||||
assert.equal(execution.summary.remapRuntimeReady, true, `${filename} remap ready`);
|
||||
assert.equal(execution.resultText.includes("fiveaxis_ini_open=1"), true, `${filename} INI proof`);
|
||||
assert.equal(execution.resultText.includes("fiveaxis_remaps_ready=1"), true, `${filename} remap proof`);
|
||||
assert.equal(execution.resultText.includes("fiveaxis_file_reached_exit=1"), true, `${filename} exit proof`);
|
||||
assert.equal(execution.machineFilePlan.selectedProgramFilename, filename);
|
||||
assert.equal(execution.machineFilePlan.selectedProgramSourceRel, source.sourceRel);
|
||||
}
|
||||
|
||||
const store = createSimulationStore();
|
||||
assert.equal(store.getState().programExecutionSourceMode, "fixture-line-playback");
|
||||
assert.equal(store.getState().programExecution, null);
|
||||
assert.equal(store.getState().fullExecutionBoundary.promotionAllowed, false);
|
||||
|
||||
assert.equal(canonicalReports.length >= 5, true);
|
||||
assert.equal(canonicalReports.every((report) => report.previewPoints > 0), true);
|
||||
assert.equal(canonicalReports.every((report) => report.executedPathPoints > 0), true);
|
||||
assert.equal(canonicalReports.every((report) => Number.isFinite(Number(report.currentLine))), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename === "boat-xyzac.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename === "boat-xyzbc.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename === "impeller-7bl-xyzac.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.filename.startsWith("xyzac_switchkins")), true);
|
||||
assert.equal(sourceByFilename.has("xyzbc_switchkins.ngc"), true);
|
||||
assert.equal(canonicalReports.some((report) => report.switchkinsEvents > 0), true);
|
||||
|
||||
console.log(`linuxcnc_source_program_case_count=${canonicalReports.length + ENTRY_MACHINE_FILE_CASES.length}`);
|
||||
console.log("all_cases_program_preview_points=ok");
|
||||
console.log("all_cases_executed_path_points=ok_after_run_or_step");
|
||||
console.log("all_cases_toolpath_preview_source=linuxcnc_interpreter_canonical_motion");
|
||||
console.log("all_cases_tool_execution_trace_source=linuxcnc_tp_samples_or_task_motion_hal_feedback");
|
||||
console.log("all_switchkins_cases_rtcp_state_changes_verified=1");
|
||||
console.log("all_cases_source_guard=linuxcnc_vendored_5axis_gcode_source_file");
|
||||
console.log("fixture_toolpath_fallback_not_promoted=1");
|
||||
console.log("real_linuxcnc_5axis_program_cases_smoke=ok");
|
||||
Reference in New Issue
Block a user