From d8e0196c61754848fd09dcb37335e1815ef5e8fe Mon Sep 17 00:00:00 2001 From: wangdequan Date: Fri, 19 Jun 2026 06:36:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BB=BA=E7=AB=8BToolDbProcessPort=E5=90=88?= =?UTF-8?q?=E7=BA=A6=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:已新增ToolDbProcessPort合约、OPFS tool DB flat-file/transcript store和Node/WASM gates,baseline保持28/28/131/0且promotion继续锁定。 --- wasm-port/runtime/opfs/path-model.js | 31 +++ wasm-port/runtime/opfs/tool-db-store.js | 78 ++++++ wasm-port/runtime/sdk/README.md | 20 ++ wasm-port/runtime/sdk/src/index.js | 18 ++ .../runtime/sdk/src/tool-db-process-port.js | 241 ++++++++++++++++++ .../tests/opfs/node/verify_file_service.mjs | 9 + .../tests/opfs/node/verify_tool_db_store.mjs | 110 ++++++++ .../tests/opfs/node/verify_tool_db_store.sh | 6 + .../tests/sdk/node/verify_sdk_surface.mjs | 22 ++ .../sdk/node/verify_tool_db_process_port.mjs | 95 +++++++ .../sdk/node/verify_tool_db_process_port.sh | 6 + .../node/verify_tool_db_process_port_wasm.mjs | 69 +++++ .../node/verify_tool_db_process_port_wasm.sh | 6 + 13 files changed, 711 insertions(+) create mode 100644 wasm-port/runtime/opfs/tool-db-store.js create mode 100644 wasm-port/runtime/sdk/src/tool-db-process-port.js create mode 100644 wasm-port/tests/opfs/node/verify_tool_db_store.mjs create mode 100644 wasm-port/tests/opfs/node/verify_tool_db_store.sh create mode 100644 wasm-port/tests/sdk/node/verify_tool_db_process_port.mjs create mode 100644 wasm-port/tests/sdk/node/verify_tool_db_process_port.sh create mode 100644 wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.mjs create mode 100644 wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.sh diff --git a/wasm-port/runtime/opfs/path-model.js b/wasm-port/runtime/opfs/path-model.js index fe6ac86..fc2ceee 100644 --- a/wasm-port/runtime/opfs/path-model.js +++ b/wasm-port/runtime/opfs/path-model.js @@ -61,6 +61,37 @@ export function toolTablePath(machineId, filename = "tool.tbl") { ); } +export function toolDbRootPath(machineId) { + return opfsPath( + ROOT, + "machines", + assertSegment(machineId, "machine id"), + "tool-db", + ); +} + +export function toolDbFilePath(machineId, filename = "db_nonran_file") { + return opfsPath( + ROOT, + "machines", + assertSegment(machineId, "machine id"), + "tool-db", + assertSegment(filename, "tool DB filename"), + ); +} + +export function toolDbTranscriptPath(machineId, runId, filename = "transcript.json") { + return opfsPath( + ROOT, + "machines", + assertSegment(machineId, "machine id"), + "tool-db", + "transcripts", + assertSegment(runId, "tool DB run id"), + assertSegment(filename, "tool DB transcript filename"), + ); +} + export function parameterFilePath(machineId, filename = "linuxcnc.var") { return opfsPath( ROOT, diff --git a/wasm-port/runtime/opfs/tool-db-store.js b/wasm-port/runtime/opfs/tool-db-store.js new file mode 100644 index 0000000..49f43b2 --- /dev/null +++ b/wasm-port/runtime/opfs/tool-db-store.js @@ -0,0 +1,78 @@ +import { loadTextFile, saveTextFile } from "./file-service.js"; +import { toolDbFilePath, toolDbTranscriptPath } from "./path-model.js"; + +function stableTextHash(text) { + let hash = 0x811c9dc5; + for (let index = 0; index < text.length; index += 1) { + hash ^= text.charCodeAt(index); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + return hash.toString(16).padStart(8, "0"); +} + +function displayOpfsPath(path) { + return path.startsWith("linuxcnc/") ? path.slice("linuxcnc".length) : `/${path}`; +} + +export function createToolDbStorePaths(machineId, { + dbFilename = "db_nonran_file", + runId = "run-1", + transcriptFilename = "transcript.json", +} = {}) { + return { + dbFile: toolDbFilePath(machineId, dbFilename), + transcript: toolDbTranscriptPath(machineId, runId, transcriptFilename), + }; +} + +export async function saveToolDbFile(machineId, text, options = {}) { + const path = options.path ?? toolDbFilePath(machineId, options.filename); + await saveTextFile(path, text, options.storage); + return { + opfsPath: path, + displayPath: displayOpfsPath(path), + dbFileHash: stableTextHash(text), + savedText: text, + }; +} + +export async function loadToolDbFile(machineId, options = {}) { + const path = options.path ?? toolDbFilePath(machineId, options.filename); + const text = await loadTextFile(path, options.storage); + return { + opfsPath: path, + displayPath: displayOpfsPath(path), + dbFileHash: stableTextHash(text), + text, + }; +} + +export async function saveToolDbTranscript(machineId, runId, transcript, options = {}) { + if (!Array.isArray(transcript)) { + throw new Error("transcript must be an array."); + } + const path = options.path ?? toolDbTranscriptPath(machineId, runId, options.filename); + const text = JSON.stringify({ + schema: "linuxcnc.toolDbProcessTranscript.v1", + transcript, + }, null, 2); + await saveTextFile(path, text, options.storage); + return { + opfsPath: path, + displayPath: displayOpfsPath(path), + transcriptHash: stableTextHash(text), + text, + }; +} + +export async function loadToolDbTranscript(machineId, runId, options = {}) { + const path = options.path ?? toolDbTranscriptPath(machineId, runId, options.filename); + const text = await loadTextFile(path, options.storage); + const payload = JSON.parse(text); + return { + opfsPath: path, + displayPath: displayOpfsPath(path), + transcriptHash: stableTextHash(text), + payload, + }; +} diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index fc0ad26..1834964 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -126,6 +126,9 @@ import { createProjectReleaseReadinessArtifactValidationSummaryViewModel, createProjectReleaseReadinessReport, createProjectReleaseReadinessSummaryViewModel, + createLinuxCncToolDbProcessPort, + createToolDbProcessDiagnostics, + createToolDbTransactionPlan, createSessionSnapshot, defaultMachinePaths, gcodeFilenameFromProgramPath, @@ -143,6 +146,8 @@ import { loadMachineSessionSnapshot, loadMachineTextFiles, loadMachineToolTableFromOpfs, + loadToolDbFile, + loadToolDbTranscript, loadTextFile, machineFilePaths, machineIniPath, @@ -183,9 +188,15 @@ import { saveMachineSessionSnapshot, saveMachineTextFiles, saveSessionSnapshot, + saveToolDbFile, + saveToolDbTranscript, saveTextFile, sessionSnapshotPath, + toolDbFilePath, + toolDbRootPath, + toolDbTranscriptPath, toolTablePath, + validateToolDbTranscript, validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson, validateSessionSnapshot, } from "./src/index.js"; @@ -534,6 +545,7 @@ private module paths: - `getOpfsRoot()`, `saveTextFile()`, and `loadTextFile()` for browser OPFS text persistence. - `machineIniPath()`, `toolTablePath()`, `parameterFilePath()`, + `toolDbRootPath()`, `toolDbFilePath()`, `toolDbTranscriptPath()`, `gcodeProgramPath()`, `sessionSnapshotPath()`, `defaultMachinePaths()`, and `normalizeOpfsPath()` for stable OPFS path construction. - `createMachineSessionSnapshotPayload()`, `createSessionSnapshot()`, @@ -552,6 +564,14 @@ private module paths: - `restoreMachineParametersFromOpfs()`, `loadMachineToolTableFromOpfs()`, and `loadMachineSessionFromOpfs()` for loading OPFS machine state into the LinuxCNC-backed interpreter SDK. +- `createLinuxCncToolDbProcessPort()`, `createToolDbTransactionPlan()`, + `validateToolDbTranscript()`, and `createToolDbProcessDiagnostics()` for the + LinuxCNC-owned tool DB process port contract. Contract-only mode does not run + `DB_PROGRAM`, does not parse `.tbl` as a fallback, and keeps execution and + promotion disabled. +- `saveToolDbFile()`, `loadToolDbFile()`, `saveToolDbTranscript()`, and + `loadToolDbTranscript()` for OPFS persistence of the DB flat file and protocol + transcript artifacts. These helpers only move and validate host files. Parameter and tool-table effects still come from the LinuxCNC-backed interpreter SDK methods they call. diff --git a/wasm-port/runtime/sdk/src/index.js b/wasm-port/runtime/sdk/src/index.js index 97dc993..4a890ec 100644 --- a/wasm-port/runtime/sdk/src/index.js +++ b/wasm-port/runtime/sdk/src/index.js @@ -91,6 +91,14 @@ export { planIniFileContextStaging, planSimConfigStaging, } from "./sim-config-staging.js"; +export { + TOOL_DB_PROCESS_PORT_CONTRACT_VERSION, + TOOL_DB_TRANSACTION_PLAN, + createLinuxCncToolDbProcessPort, + createToolDbProcessDiagnostics, + createToolDbTransactionPlan, + validateToolDbTranscript, +} from "./tool-db-process-port.js"; export { defaultMachinePaths, @@ -103,6 +111,9 @@ export { previewCachePath, sessionSnapshotPath, splitOpfsPath, + toolDbFilePath, + toolDbRootPath, + toolDbTranscriptPath, toolTablePath, } from "../../opfs/path-model.js"; export { @@ -138,6 +149,13 @@ export { loadMachineToolTableFromOpfs, saveMachineToolTableToOpfs, } from "../../opfs/linuxcnc-tool-table-bridge.js"; +export { + createToolDbStorePaths, + loadToolDbFile, + loadToolDbTranscript, + saveToolDbFile, + saveToolDbTranscript, +} from "../../opfs/tool-db-store.js"; export { loadMachineSessionFromOpfs } from "../../opfs/linuxcnc-machine-session-bridge.js"; export { readMachineSessionReadiness } from "../../opfs/machine-session-readiness.js"; diff --git a/wasm-port/runtime/sdk/src/tool-db-process-port.js b/wasm-port/runtime/sdk/src/tool-db-process-port.js new file mode 100644 index 0000000..e3337d4 --- /dev/null +++ b/wasm-port/runtime/sdk/src/tool-db-process-port.js @@ -0,0 +1,241 @@ +const TOOL_DB_PROTOCOL_VERSION = "v2.1"; + +export const TOOL_DB_PROCESS_PORT_CONTRACT_VERSION = 1; + +export const TOOL_DB_TRANSACTION_PLAN = [ + { + phase: "startup_handshake", + direction: "read", + expect: TOOL_DB_PROTOCOL_VERSION, + requiresFini: false, + }, + { + phase: "initial_get_all", + direction: "write", + line: "g", + expect: "T10..T19", + requiresFini: true, + }, + { + phase: "tool_offset_notify", + direction: "write", + line: "p t11 p111 d0.33 z0.11", + expect: "FINI", + requiresFini: true, + }, + { + phase: "spindle_load_notify", + direction: "write", + line: "l t14 p0", + expect: "T14 P0 after query", + requiresFini: true, + }, + { + phase: "spindle_unload_notify", + direction: "write", + line: "u t0 p0", + expect: "T14 P114 after query", + requiresFini: true, + }, +]; + +function requireString(value, label) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`${label} must be a non-empty string.`); + } + return value; +} + +function normalizeSourceFiles(sourceFiles = []) { + if (!Array.isArray(sourceFiles)) { + throw new Error("sourceFiles must be an array."); + } + return sourceFiles.map((sourceFile) => requireString(sourceFile, "source file")); +} + +function stableTextHash(text) { + let hash = 0x811c9dc5; + for (let index = 0; index < text.length; index += 1) { + hash ^= text.charCodeAt(index); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + return hash.toString(16).padStart(8, "0"); +} + +function transcriptText(transcript) { + return transcript + .map((entry) => `${entry.direction}:${entry.line}`) + .join("\n"); +} + +export function createToolDbTransactionPlan() { + return TOOL_DB_TRANSACTION_PLAN.map((step) => ({ ...step })); +} + +export function createToolDbProcessDiagnostics({ + dbProgramPath, + opfsDbPath = "-", + transcript = [], + dbFileText = "", + startupToolCount = 0, + mutationCount = 0, + runtimeMode = "contract-only", + runtimeExecutionReady = false, +} = {}) { + requireString(dbProgramPath, "dbProgramPath"); + const transcriptHash = stableTextHash(transcriptText(transcript)); + const dbFileHash = stableTextHash(dbFileText); + + return { + contractVersion: TOOL_DB_PROCESS_PORT_CONTRACT_VERSION, + dbProgramPath, + opfsDbPath, + runtimeMode, + runtimeExecutionReady: runtimeExecutionReady === true, + protocolTranscriptReady: transcript.length > 0, + opfsPersistenceReady: dbFileText.length > 0, + transcriptHash, + dbFileHash, + startupToolCount, + mutationCount, + tblFallbackSufficient: false, + executionEnabled: false, + promotionAllowed: false, + }; +} + +export function validateToolDbTranscript(transcript) { + if (!Array.isArray(transcript)) { + throw new Error("transcript must be an array."); + } + const lines = transcript.map((entry) => entry?.line ?? ""); + const hasVersion = lines.includes(TOOL_DB_PROTOCOL_VERSION); + const hasGetAll = lines.includes("g"); + const hasFini = lines.some((line) => line.startsWith("FINI")); + const hasPut = lines.includes("p t11 p111 d0.33 z0.11"); + const hasLoad = lines.includes("l t14 p0"); + const hasUnload = lines.includes("u t0 p0"); + + return { + protocolVersion: TOOL_DB_PROTOCOL_VERSION, + hasVersion, + hasGetAll, + hasFini, + hasPut, + hasLoad, + hasUnload, + ready: hasVersion && hasGetAll && hasFini && hasPut && hasLoad && hasUnload, + }; +} + +export function createLinuxCncToolDbProcessPort({ + dbProgramPath, + sourceFiles = [], + opfsRoot = null, + runtimeMode = "contract-only", + runtimeAdapter = null, +} = {}) { + requireString(dbProgramPath, "dbProgramPath"); + const normalizedSourceFiles = normalizeSourceFiles(sourceFiles); + const transcript = []; + let started = false; + let closed = false; + + function assertOpen() { + if (closed) { + throw new Error("ToolDbProcessPort is closed."); + } + } + + return { + dbProgramPath, + sourceFiles: normalizedSourceFiles, + opfsRoot, + runtimeMode, + + async start() { + assertOpen(); + started = true; + if (runtimeAdapter?.start) { + await runtimeAdapter.start(); + } + return { + runtimeMode, + runtimeExecutionReady: Boolean(runtimeAdapter), + executionEnabled: false, + promotionAllowed: false, + }; + }, + + async writeLine(line) { + assertOpen(); + if (!started) { + throw new Error("ToolDbProcessPort must be started before writeLine()."); + } + const normalizedLine = requireString(line, "line"); + transcript.push({ direction: "write", line: normalizedLine }); + if (runtimeAdapter?.writeLine) { + await runtimeAdapter.writeLine(normalizedLine); + } + }, + + async readLine() { + assertOpen(); + if (!started) { + throw new Error("ToolDbProcessPort must be started before readLine()."); + } + if (!runtimeAdapter?.readLine) { + throw new Error("ToolDbProcessPort has no runtime adapter; contract-only mode cannot read DB_PROGRAM output."); + } + const line = await runtimeAdapter.readLine(); + transcript.push({ direction: "read", line }); + return line; + }, + + async runTransactionPlan(plan = createToolDbTransactionPlan()) { + assertOpen(); + if (!runtimeAdapter) { + return { + status: "blocked_runtime_adapter_required", + plan, + runtimeExecutionReady: false, + executionEnabled: false, + promotionAllowed: false, + tblFallbackSufficient: false, + }; + } + for (const step of plan) { + if (step.direction === "write") { + await this.writeLine(step.line); + } else { + await this.readLine(); + } + } + return { + status: "runtime_adapter_transaction_plan_executed", + plan, + transcript: this.exportTranscript(), + runtimeExecutionReady: true, + executionEnabled: false, + promotionAllowed: false, + tblFallbackSufficient: false, + }; + }, + + exportTranscript() { + return transcript.map((entry) => ({ ...entry })); + }, + + close() { + if (runtimeAdapter?.close) { + runtimeAdapter.close(); + } + closed = true; + return { + closed, + executionEnabled: false, + promotionAllowed: false, + }; + }, + }; +} diff --git a/wasm-port/tests/opfs/node/verify_file_service.mjs b/wasm-port/tests/opfs/node/verify_file_service.mjs index 9f1ceeb..1669f08 100644 --- a/wasm-port/tests/opfs/node/verify_file_service.mjs +++ b/wasm-port/tests/opfs/node/verify_file_service.mjs @@ -13,6 +13,9 @@ import { parameterFilePath, previewCachePath, sessionSnapshotPath, + toolDbFilePath, + toolDbRootPath, + toolDbTranscriptPath, toolTablePath, } from "../../../runtime/opfs/path-model.js"; import { @@ -205,6 +208,12 @@ assert.equal(await getOpfsRoot(storage), root); assert.equal(normalizeOpfsPath("linuxcnc/machines/xyzab.ini"), "linuxcnc/machines/xyzab.ini"); assert.equal(machineIniPath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/machine.ini"); assert.equal(toolTablePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/tool.tbl"); +assert.equal(toolDbRootPath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/tool-db"); +assert.equal(toolDbFilePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/tool-db/db_nonran_file"); +assert.equal( + toolDbTranscriptPath("xyzab-tdr", "run-1"), + "linuxcnc/machines/xyzab-tdr/tool-db/transcripts/run-1/transcript.json", +); assert.equal(parameterFilePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/linuxcnc.var"); assertGcodeProgramStoragePath("fixture.ngc", gcodeProgramPath("fixture.ngc")); assertGcodeProgramStoragePath( diff --git a/wasm-port/tests/opfs/node/verify_tool_db_store.mjs b/wasm-port/tests/opfs/node/verify_tool_db_store.mjs new file mode 100644 index 0000000..b0559e2 --- /dev/null +++ b/wasm-port/tests/opfs/node/verify_tool_db_store.mjs @@ -0,0 +1,110 @@ +import assert from "node:assert/strict"; + +import { + createToolDbStorePaths, + loadToolDbFile, + loadToolDbTranscript, + saveToolDbFile, + saveToolDbTranscript, +} from "../../../runtime/opfs/tool-db-store.js"; +import { + toolDbFilePath, + toolDbRootPath, + toolDbTranscriptPath, +} from "../../../runtime/opfs/path-model.js"; + +class MockFileHandle { + constructor(name) { + this.name = name; + this.textValue = ""; + } + + async createWritable() { + return { + write: async (text) => { + this.textValue = String(text); + }, + close: async () => {}, + }; + } + + async getFile() { + return { + text: async () => this.textValue, + }; + } +} + +class MockDirectoryHandle { + constructor(name = "") { + this.name = name; + this.dirs = new Map(); + this.files = new Map(); + } + + async getDirectoryHandle(name, options = {}) { + if (!this.dirs.has(name)) { + if (!options.create) { + throw new Error(`missing directory: ${name}`); + } + this.dirs.set(name, new MockDirectoryHandle(name)); + } + return this.dirs.get(name); + } + + async getFileHandle(name, options = {}) { + if (!this.files.has(name)) { + if (!options.create) { + throw new Error(`missing file: ${name}`); + } + this.files.set(name, new MockFileHandle(name)); + } + return this.files.get(name); + } +} + +const root = new MockDirectoryHandle(); +const storage = { + getDirectory: async () => root, +}; + +assert.equal(toolDbRootPath("db-demo"), "linuxcnc/machines/db-demo/tool-db"); +assert.equal(toolDbFilePath("db-demo"), "linuxcnc/machines/db-demo/tool-db/db_nonran_file"); +assert.equal( + toolDbTranscriptPath("db-demo", "run-42"), + "linuxcnc/machines/db-demo/tool-db/transcripts/run-42/transcript.json", +); +assert.deepEqual(createToolDbStorePaths("db-demo", { runId: "run-42" }), { + dbFile: "linuxcnc/machines/db-demo/tool-db/db_nonran_file", + transcript: "linuxcnc/machines/db-demo/tool-db/transcripts/run-42/transcript.json", +}); + +const dbText = "T11 P111 D0.33 Z0.11\nT14 P114 D0.14 Z0.14\n"; +const savedDb = await saveToolDbFile("db-demo", dbText, { storage }); +assert.equal(savedDb.opfsPath, "linuxcnc/machines/db-demo/tool-db/db_nonran_file"); +assert.equal(savedDb.displayPath, "/machines/db-demo/tool-db/db_nonran_file"); +assert.match(savedDb.dbFileHash, /^[0-9a-f]{8}$/); + +const loadedDb = await loadToolDbFile("db-demo", { storage }); +assert.equal(loadedDb.text, dbText); +assert.equal(loadedDb.dbFileHash, savedDb.dbFileHash); + +const transcript = [ + { direction: "read", line: "v2.1" }, + { direction: "write", line: "g" }, + { direction: "read", line: "FINI (get_cmd)" }, +]; +const savedTranscript = await saveToolDbTranscript("db-demo", "run-42", transcript, { storage }); +assert.equal( + savedTranscript.opfsPath, + "linuxcnc/machines/db-demo/tool-db/transcripts/run-42/transcript.json", +); +assert.equal(savedTranscript.displayPath, "/machines/db-demo/tool-db/transcripts/run-42/transcript.json"); +assert.match(savedTranscript.transcriptHash, /^[0-9a-f]{8}$/); + +const loadedTranscript = await loadToolDbTranscript("db-demo", "run-42", { storage }); +assert.equal(loadedTranscript.payload.schema, "linuxcnc.toolDbProcessTranscript.v1"); +assert.deepEqual(loadedTranscript.payload.transcript, transcript); +assert.equal(loadedTranscript.transcriptHash, savedTranscript.transcriptHash); + +console.log("tool_db_store_opfs=ok"); diff --git a/wasm-port/tests/opfs/node/verify_tool_db_store.sh b/wasm-port/tests/opfs/node/verify_tool_db_store.sh new file mode 100644 index 0000000..4c0ffda --- /dev/null +++ b/wasm-port/tests/opfs/node/verify_tool_db_store.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" + +node "$ROOT_DIR/tests/opfs/node/verify_tool_db_store.mjs" diff --git a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs index 92123b8..e2afb21 100644 --- a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs +++ b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs @@ -90,6 +90,9 @@ import { createProjectReleaseReadinessArtifactValidationSummaryViewModel, createProjectReleaseReadinessReport, createProjectReleaseReadinessSummaryViewModel, + createLinuxCncToolDbProcessPort, + createToolDbProcessDiagnostics, + createToolDbTransactionPlan, createVirtualHalBridgeActionPlan, createVirtualHalBridgeReadiness, createVirtualHalCommandScriptFixtureReport, @@ -153,6 +156,8 @@ import { loadMachineSessionSnapshot, loadMachineTextFiles, loadMachineToolTableFromOpfs, + loadToolDbFile, + loadToolDbTranscript, loadTextFile, machineFilePaths, machineIniPath, @@ -172,7 +177,13 @@ import { saveSessionSnapshot, saveTextFile, sessionSnapshotPath, + saveToolDbFile, + saveToolDbTranscript, + toolDbFilePath, + toolDbRootPath, + toolDbTranscriptPath, toolTablePath, + validateToolDbTranscript, validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson, validateSessionSnapshot, } from "../../../runtime/sdk/src/index.js"; @@ -490,6 +501,9 @@ const requiredExports = [ ["loadTextFile", loadTextFile], ["machineIniPath", machineIniPath], ["toolTablePath", toolTablePath], + ["toolDbRootPath", toolDbRootPath], + ["toolDbFilePath", toolDbFilePath], + ["toolDbTranscriptPath", toolDbTranscriptPath], ["parameterFilePath", parameterFilePath], ["gcodeProgramPath", gcodeProgramPath], ["sessionSnapshotPath", sessionSnapshotPath], @@ -507,6 +521,14 @@ const requiredExports = [ ["saveMachineTextFiles", saveMachineTextFiles], ["loadMachineTextFiles", loadMachineTextFiles], ["gcodeFilenameFromProgramPath", gcodeFilenameFromProgramPath], + ["createLinuxCncToolDbProcessPort", createLinuxCncToolDbProcessPort], + ["createToolDbProcessDiagnostics", createToolDbProcessDiagnostics], + ["createToolDbTransactionPlan", createToolDbTransactionPlan], + ["validateToolDbTranscript", validateToolDbTranscript], + ["saveToolDbFile", saveToolDbFile], + ["loadToolDbFile", loadToolDbFile], + ["saveToolDbTranscript", saveToolDbTranscript], + ["loadToolDbTranscript", loadToolDbTranscript], ["restoreMachineParametersFromOpfs", restoreMachineParametersFromOpfs], ["loadMachineToolTableFromOpfs", loadMachineToolTableFromOpfs], ["loadMachineSessionFromOpfs", loadMachineSessionFromOpfs], diff --git a/wasm-port/tests/sdk/node/verify_tool_db_process_port.mjs b/wasm-port/tests/sdk/node/verify_tool_db_process_port.mjs new file mode 100644 index 0000000..2828d1a --- /dev/null +++ b/wasm-port/tests/sdk/node/verify_tool_db_process_port.mjs @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; + +import { + TOOL_DB_PROCESS_PORT_CONTRACT_VERSION, + TOOL_DB_TRANSACTION_PLAN, + createLinuxCncToolDbProcessPort, + createToolDbProcessDiagnostics, + createToolDbTransactionPlan, + validateToolDbTranscript, +} from "../../../runtime/sdk/src/tool-db-process-port.js"; + +assert.equal(TOOL_DB_PROCESS_PORT_CONTRACT_VERSION, 1); +assert.deepEqual( + createToolDbTransactionPlan().map((step) => step.phase), + [ + "startup_handshake", + "initial_get_all", + "tool_offset_notify", + "spindle_load_notify", + "spindle_unload_notify", + ], +); +assert.equal(TOOL_DB_TRANSACTION_PLAN[0].expect, "v2.1"); +assert.equal(TOOL_DB_TRANSACTION_PLAN.every((step) => step.requiresFini || step.phase === "startup_handshake"), true); + +const port = createLinuxCncToolDbProcessPort({ + dbProgramPath: "./db_nonran.py", + sourceFiles: [ + "configs/sim/axis/db_demo/db_nonran.py", + "configs/sim/axis/db_demo/db.py", + "lib/python/tooldb.py", + ], + runtimeMode: "contract-only", +}); + +const startResult = await port.start(); +assert.deepEqual(startResult, { + runtimeMode: "contract-only", + runtimeExecutionReady: false, + executionEnabled: false, + promotionAllowed: false, +}); + +const blockedPlan = await port.runTransactionPlan(); +assert.equal(blockedPlan.status, "blocked_runtime_adapter_required"); +assert.equal(blockedPlan.runtimeExecutionReady, false); +assert.equal(blockedPlan.executionEnabled, false); +assert.equal(blockedPlan.promotionAllowed, false); +assert.equal(blockedPlan.tblFallbackSufficient, false); + +const transcript = [ + { direction: "read", line: "v2.1" }, + { direction: "write", line: "g" }, + { direction: "read", line: "T10 P110 D0.10 Z0.10" }, + { direction: "read", line: "FINI (get_cmd)" }, + { direction: "write", line: "p t11 p111 d0.33 z0.11" }, + { direction: "read", line: "FINI (update recvd) t11 p111 d0.33 z0.11" }, + { direction: "write", line: "l t14 p0" }, + { direction: "read", line: "FINI (update recvd) t14 p0" }, + { direction: "write", line: "u t0 p0" }, + { direction: "read", line: "FINI (update recvd) t0 p0" }, +]; +const validation = validateToolDbTranscript(transcript); +assert.equal(validation.ready, true); +assert.equal(validation.hasVersion, true); +assert.equal(validation.hasGetAll, true); +assert.equal(validation.hasFini, true); +assert.equal(validation.hasPut, true); +assert.equal(validation.hasLoad, true); +assert.equal(validation.hasUnload, true); + +const diagnostics = createToolDbProcessDiagnostics({ + dbProgramPath: "./db_nonran.py", + opfsDbPath: "/machines/db-demo/tool-db/db_nonran_file", + transcript, + dbFileText: "T11 P111 D0.33 Z0.11\nT14 P114 D0.14 Z0.14\n", + startupToolCount: 10, + mutationCount: 3, + runtimeMode: "wasm-tool-db-protocol-worker", +}); +assert.equal(diagnostics.dbProgramPath, "./db_nonran.py"); +assert.equal(diagnostics.protocolTranscriptReady, true); +assert.equal(diagnostics.opfsPersistenceReady, true); +assert.equal(diagnostics.startupToolCount, 10); +assert.equal(diagnostics.mutationCount, 3); +assert.equal(diagnostics.tblFallbackSufficient, false); +assert.equal(diagnostics.runtimeExecutionReady, false); +assert.equal(diagnostics.executionEnabled, false); +assert.equal(diagnostics.promotionAllowed, false); +assert.match(diagnostics.transcriptHash, /^[0-9a-f]{8}$/); +assert.match(diagnostics.dbFileHash, /^[0-9a-f]{8}$/); + +assert.equal(port.close().promotionAllowed, false); + +console.log("tool_db_process_port_sdk=ok"); diff --git a/wasm-port/tests/sdk/node/verify_tool_db_process_port.sh b/wasm-port/tests/sdk/node/verify_tool_db_process_port.sh new file mode 100644 index 0000000..66938e8 --- /dev/null +++ b/wasm-port/tests/sdk/node/verify_tool_db_process_port.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" + +node "$ROOT_DIR/tests/sdk/node/verify_tool_db_process_port.mjs" diff --git a/wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.mjs b/wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.mjs new file mode 100644 index 0000000..27a4ca3 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.mjs @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { + createLinuxCncToolDbProcessPort, + createToolDbProcessDiagnostics, + createToolDbTransactionPlan, + validateToolDbTranscript, +} from "../../../runtime/sdk/src/tool-db-process-port.js"; +import { + createToolDbStorePaths, +} from "../../../runtime/opfs/tool-db-store.js"; + +const iniText = readFileSync( + resolve("linuxcnc/configs/sim/axis/db_demo/db_nonran.ini"), + "utf8", +); +assert.match(iniText, /DB_PROGRAM\s*=\s*\.\/db_nonran\.py/); + +const plan = createToolDbTransactionPlan(); +assert.equal(plan.length, 5); +assert.equal(plan[0].expect, "v2.1"); +assert.equal(plan.some((step) => step.line === "g"), true); +assert.equal(plan.some((step) => step.line === "p t11 p111 d0.33 z0.11"), true); +assert.equal(plan.some((step) => step.line === "l t14 p0"), true); +assert.equal(plan.some((step) => step.line === "u t0 p0"), true); + +const port = createLinuxCncToolDbProcessPort({ + dbProgramPath: "./db_nonran.py", + sourceFiles: [ + "configs/sim/axis/db_demo/db_nonran.py", + "configs/sim/axis/db_demo/db.py", + "lib/python/tooldb.py", + ], + runtimeMode: "contract-only", +}); +assert.equal((await port.start()).runtimeExecutionReady, false); +assert.equal((await port.runTransactionPlan()).status, "blocked_runtime_adapter_required"); + +const transcript = [ + { direction: "read", line: "v2.1" }, + { direction: "write", line: "g" }, + { direction: "read", line: "T10 P110 D0.10 Z0.10" }, + { direction: "read", line: "FINI (get_cmd)" }, + { direction: "write", line: "p t11 p111 d0.33 z0.11" }, + { direction: "write", line: "l t14 p0" }, + { direction: "write", line: "u t0 p0" }, +]; +assert.equal(validateToolDbTranscript(transcript).ready, true); + +const paths = createToolDbStorePaths("db-demo", { runId: "run-1" }); +assert.equal(paths.dbFile, "linuxcnc/machines/db-demo/tool-db/db_nonran_file"); +assert.equal(paths.transcript, "linuxcnc/machines/db-demo/tool-db/transcripts/run-1/transcript.json"); + +const diagnostics = createToolDbProcessDiagnostics({ + dbProgramPath: "./db_nonran.py", + opfsDbPath: "/machines/db-demo/tool-db/db_nonran_file", + transcript, + dbFileText: "T11 P111 D0.33 Z0.11\n", + startupToolCount: 10, + mutationCount: 3, +}); +assert.equal(diagnostics.tblFallbackSufficient, false); +assert.equal(diagnostics.runtimeExecutionReady, false); +assert.equal(diagnostics.executionEnabled, false); +assert.equal(diagnostics.promotionAllowed, false); + +console.log("tool_db_process_port_wasm=ok"); diff --git a/wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.sh b/wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.sh new file mode 100644 index 0000000..c019cc8 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" + +node "$ROOT_DIR/tests/wasm/node/verify_tool_db_process_port_wasm.mjs"