新增 OPFS session 独立 browser workflow gate

This commit is contained in:
2026-06-16 17:30:23 +08:00
parent 741afc5103
commit 35bc25714d
18 changed files with 478 additions and 51 deletions

View File

@@ -138,7 +138,7 @@
"workflow overview release artifact validation API name",
);
assertEqual(releaseArtifactValidation.ready, true, "workflow overview release artifact validation readiness");
assertEqual(releaseArtifactValidation.gateCount, 10, "workflow overview release artifact validation gates");
assertEqual(releaseArtifactValidation.gateCount, 11, "workflow overview release artifact validation gates");
assertEqual(
releaseArtifactValidationSummary.statusLine,
"Ready: Artifact evidence complete",

View File

@@ -0,0 +1,203 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>LinuxCNC OPFS Session Workflow Smoke</title>
</head>
<body>
<pre id="status">running</pre>
<script type="module">
import {
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
createMachineSessionPersistenceSummary,
gcodeProgramPath,
loadGcodeProgram,
loadMachineSessionFromOpfs,
loadMachineSessionSnapshot,
loadMachineTextFiles,
machineFilePaths,
readMachineSessionReadiness,
saveGcodeProgram,
saveMachineSessionSnapshot,
saveMachineTextFiles,
} from "../../runtime/sdk/src/index.js";
const status = document.getElementById("status");
function assertEqual(actual, expected, label) {
if (actual !== expected) {
throw new Error(`${label}: expected ${expected}, got ${actual}`);
}
}
function assertIncludes(values, expected, label) {
if (!values.includes(expected)) {
throw new Error(`${label}: expected ${expected} in ${values.join(", ")}`);
}
}
try {
const machineId = "browser-opfs-session-gate";
const sessionId = "browser-opfs-session-gate-session";
const snapshotFilename = "browser-opfs-session-gate.json";
const gcodeFilename = "browser-opfs-session-gate.ngc";
const gcodeOpfsPath = gcodeProgramPath(gcodeFilename);
const paths = machineFilePaths(machineId);
const iniText = `[EMC]
MACHINE = browser-opfs-session-gate
[TRAJ]
LINEAR_UNITS = mm
COORDINATES = X Y Z
[KINS]
KINEMATICS = trivkins
JOINTS = 3
[RS274NGC]
PARAMETER_FILE = linuxcnc.var
[EMCIO]
TOOL_TABLE = tool.tbl
`;
const parameterText = [
"5161 0.0",
"5162 0.0",
"5220 1",
"5221 0.0",
"5399 0.0",
"",
].join("\n");
const toolTableText = "T1 P1 Z0.0 D0.125 ;browser opfs session gate\n";
const gcodeText = "G0 X0 Y0\nG1 X1 Y1 F20\nM2\n";
const initialReadiness = await readMachineSessionReadiness(machineId, {
gcodeFilename,
sessionId,
snapshotFilename,
});
assertEqual(initialReadiness.ready, false, "initial readiness");
assertIncludes(initialReadiness.missing, "ini", "initial missing INI");
assertIncludes(initialReadiness.missing, "parameters", "initial missing parameters");
assertIncludes(initialReadiness.missing, "toolTable", "initial missing tool table");
assertIncludes(initialReadiness.missing, "gcode", "initial missing G-code");
assertIncludes(initialReadiness.missing, "snapshot", "initial missing snapshot");
await saveMachineTextFiles(machineId, {
ini: iniText,
parameters: parameterText,
toolTable: toolTableText,
});
await saveGcodeProgram(gcodeFilename, gcodeText);
const snapshot = await saveMachineSessionSnapshot(sessionId, machineId, {
filename: snapshotFilename,
gcodeFilename,
createdAt: "2026-06-16T00:00:00.000Z",
metadata: {
workflow: "opfs-session-workflow-browser-gate",
defaultGcodeOpfsPath: gcodeOpfsPath,
snapshotLabel: `${sessionId}/${snapshotFilename}`,
},
});
const files = await loadMachineTextFiles(machineId);
assertEqual(files.ini, iniText, "loaded INI text");
assertEqual(files.parameters, parameterText, "loaded parameter text");
assertEqual(files.toolTable, toolTableText, "loaded tool table text");
assertEqual(await loadGcodeProgram(gcodeFilename), gcodeText, "loaded G-code text");
assertEqual(snapshot.payload.files.ini, paths.ini, "snapshot INI path");
assertEqual(snapshot.payload.files.parameters, paths.parameters, "snapshot parameter path");
assertEqual(snapshot.payload.files.toolTable, paths.toolTable, "snapshot tool table path");
assertEqual(snapshot.payload.files.gcode, gcodeOpfsPath, "snapshot G-code path");
assertEqual(
(await loadMachineSessionSnapshot(sessionId, machineId, { filename: snapshotFilename }))
.metadata.workflow,
"opfs-session-workflow-browser-gate",
"loaded snapshot workflow metadata",
);
const readyReadiness = await readMachineSessionReadiness(machineId, {
gcodeFilename,
sessionId,
snapshotFilename,
});
assertEqual(readyReadiness.ready, true, "ready readiness");
assertEqual(readyReadiness.phase, "ready", "ready phase");
assertEqual(readyReadiness.missing.length, 0, "ready missing count");
assertEqual(readyReadiness.checks.length, 4, "ready file check count");
assertEqual(readyReadiness.snapshotCheck.ok, true, "ready snapshot check");
const iniSdk = await createLinuxCncIniSdk();
const interpSdk = await createLinuxCncInterpSdk();
const loadedSession = await loadMachineSessionFromOpfs(interpSdk, machineId, {
iniSdk,
iniWasmPath: "/work/opfs-session-workflow.ini",
parameterWasmPath: "/work/opfs-session-workflow.var",
toolTableWasmPath: "/work/opfs-session-workflow.tbl",
});
assertEqual(loadedSession.machineId, machineId, "loaded session machine");
assertEqual(loadedSession.ini.opfsPath, paths.ini, "loaded session INI OPFS path");
assertEqual(loadedSession.ini.wasmPath, "/work/opfs-session-workflow.ini", "loaded session INI WASM path");
assertEqual(loadedSession.parameters.opfsPath, paths.parameters, "loaded session parameter OPFS path");
assertEqual(loadedSession.parameters.wasmPath, "/work/opfs-session-workflow.var", "loaded session parameter WASM path");
assertEqual(loadedSession.toolTable.opfsPath, paths.toolTable, "loaded session tool table OPFS path");
assertEqual(loadedSession.toolTable.wasmPath, "/work/opfs-session-workflow.tbl", "loaded session tool table WASM path");
const persistenceSummary = createMachineSessionPersistenceSummary({
state: {
badges: { opfs: "OPFS: ready" },
machineFiles: {
iniOpfsPath: paths.ini,
parameterOpfsPath: paths.parameters,
toolTableOpfsPath: paths.toolTable,
gcodeOpfsPath,
},
sessionSnapshot: {
snapshotLabel: `${sessionId}/${snapshotFilename}`,
iniOpfsPath: paths.ini,
parameterOpfsPath: paths.parameters,
toolTableOpfsPath: paths.toolTable,
gcodeOpfsPath,
workflow: "opfs-session-workflow-browser-gate",
},
sessionReadiness: readyReadiness,
sessionLoad: {
iniWasmPath: loadedSession.ini.wasmPath,
parameterWasmPath: loadedSession.parameters.wasmPath,
toolTableWasmPath: loadedSession.toolTable.wasmPath,
},
},
readonlyStatus: { ready: true },
handoffWorkflowSummary: { ready: true },
});
assertEqual(persistenceSummary.apiName, "machine-session-persistence-summary", "summary API");
assertEqual(persistenceSummary.ready, true, "summary ready");
assertEqual(persistenceSummary.counts.machineFiles, 4, "summary machine file count");
assertEqual(persistenceSummary.missing.length, 0, "summary missing count");
assertEqual(
persistenceSummary.rows.find(({ id }) => id === "session-readiness")?.value,
"ready",
"summary readiness row",
);
assertEqual(
persistenceSummary.rows.find(({ id }) => id === "session-load")?.value,
"loaded: /work/opfs-session-workflow.ini",
"summary load row",
);
assertEqual(
persistenceSummary.rows.find(({ id }) => id === "handoff-readiness")?.value,
"ready",
"summary handoff row",
);
status.textContent = "browser_opfs_session_workflow_smoke=ok";
console.log("browser_opfs_session_workflow_smoke=ok");
} catch (error) {
status.textContent = `browser_opfs_session_workflow_smoke=failed: ${error.message}`;
console.error(error);
throw error;
}
</script>
</body>
</html>

View File

@@ -68,7 +68,7 @@
);
assertEqual(readyWorkflow.ready, true, "ready workflow readiness");
assertEqual(readyWorkflow.httpStatus, 200, "ready workflow HTTP status");
assertEqual(readyWorkflow.validation.gateCount, 10, "ready workflow gate count");
assertEqual(readyWorkflow.validation.gateCount, 11, "ready workflow gate count");
assertEqual(
readyWorkflow.validationSummaryViewModel.statusLine,
"Ready: Artifact evidence complete",

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
if [[ -z "$CHROMIUM" ]]; then
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
exit 1
fi
if [[ "${SKIP_INI_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_ini_panel.sh"
fi
if [[ "${SKIP_INTERP_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_wasm_core.sh"
fi
TMP_DIR="$(mktemp -d)"
PORT_FILE="$TMP_DIR/port"
SERVER_LOG="$TMP_DIR/server.log"
CHROME_PROFILE="$TMP_DIR/chrome-profile"
mkdir -p "$CHROME_PROFILE"
cleanup() {
if [[ -n "${SERVER_PID:-}" ]]; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
import functools
import http.server
import pathlib
import socketserver
import sys
root = pathlib.Path(sys.argv[1])
port_file = pathlib.Path(sys.argv[2])
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
httpd.serve_forever()
PY
SERVER_PID=$!
for _ in $(seq 1 100); do
[[ -s "$PORT_FILE" ]] && break
sleep 0.05
done
if [[ ! -s "$PORT_FILE" ]]; then
echo "OPFS/session workflow browser smoke HTTP server did not start" >&2
cat "$SERVER_LOG" >&2 || true
exit 1
fi
PORT="$(cat "$PORT_FILE")"
URL="http://127.0.0.1:$PORT/tests/browser/opfs_session_workflow_smoke.html"
OUT="$TMP_DIR/chromium.out"
"$CHROMIUM" \
--headless=new \
--disable-gpu \
--no-sandbox \
--user-data-dir="$CHROME_PROFILE" \
--virtual-time-budget=10000 \
--dump-dom \
"$URL" >"$OUT" 2>&1
if ! grep -Fq "browser_opfs_session_workflow_smoke=ok" "$OUT"; then
echo "OPFS/session workflow browser smoke failed" >&2
sed -n '1,220p' "$OUT" >&2
exit 1
fi
echo "browser_opfs_session_workflow_smoke=ok"