Add OPFS G-code staging and browser simulation checks
This commit is contained in:
@@ -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