diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index 5e1b7a8..c682432 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -569,6 +569,10 @@ private module paths: 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. +- `runToolDbProcessPortPersistenceSession()` for running a supplied tool DB + process port, persisting its transcript and DB flat file through the OPFS tool + DB store, and returning diagnostics. The helper is browser-safe glue; runtime + semantics still come from the supplied port adapter. - `createLinuxCncToolDbNodeRuntimeAdapter()` in `runtime/sdk/src/tool-db-node-runtime-adapter.js` for Node/native verification of the real LinuxCNC `DB_PROGRAM` child process. This adapter is intentionally diff --git a/wasm-port/runtime/sdk/src/index.js b/wasm-port/runtime/sdk/src/index.js index 4a890ec..809702e 100644 --- a/wasm-port/runtime/sdk/src/index.js +++ b/wasm-port/runtime/sdk/src/index.js @@ -99,6 +99,9 @@ export { createToolDbTransactionPlan, validateToolDbTranscript, } from "./tool-db-process-port.js"; +export { + runToolDbProcessPortPersistenceSession, +} from "./tool-db-runtime-session.js"; export { defaultMachinePaths, diff --git a/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js b/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js index 4266957..2d5303c 100644 --- a/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js +++ b/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js @@ -1,5 +1,5 @@ import { createWriteStream } from "node:fs"; -import { mkdir, rm } from "node:fs/promises"; +import { mkdir, readFile, rm } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; @@ -156,5 +156,9 @@ export function createLinuxCncToolDbNodeRuntimeAdapter({ stdoutFile?.end(); stderrFile?.end(); }, + + async readDbFileText() { + return readFile(dbSavefile, "utf8"); + }, }; } diff --git a/wasm-port/runtime/sdk/src/tool-db-runtime-session.js b/wasm-port/runtime/sdk/src/tool-db-runtime-session.js new file mode 100644 index 0000000..cdc1bde --- /dev/null +++ b/wasm-port/runtime/sdk/src/tool-db-runtime-session.js @@ -0,0 +1,78 @@ +import { + loadToolDbFile, + loadToolDbTranscript, + saveToolDbFile, + saveToolDbTranscript, +} from "../../opfs/tool-db-store.js"; +import { + createToolDbProcessDiagnostics, + createToolDbTransactionPlan, + validateToolDbTranscript, +} from "./tool-db-process-port.js"; + +function requireString(value, label) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`${label} must be a non-empty string.`); + } + return value; +} + +export async function runToolDbProcessPortPersistenceSession({ + port, + machineId, + runId = "run-1", + dbProgramPath = "./db_nonran.py", + dbFileText, + storage, + plan = createToolDbTransactionPlan(), + startupToolCount = 10, + mutationCount = 3, + readDbFileText = null, +} = {}) { + if (!port?.start || !port?.runTransactionPlan || !port?.exportTranscript || !port?.close) { + throw new Error("port must be a ToolDbProcessPort-like object."); + } + requireString(machineId, "machineId"); + requireString(runId, "runId"); + requireString(dbProgramPath, "dbProgramPath"); + + const startResult = await port.start(); + const planResult = await port.runTransactionPlan(plan); + const transcript = port.exportTranscript(); + const validation = validateToolDbTranscript(transcript); + const resolvedDbFileText = readDbFileText ? await readDbFileText() : (dbFileText ?? ""); + await port.close(); + + const savedDb = await saveToolDbFile(machineId, resolvedDbFileText, { storage }); + const savedTranscript = await saveToolDbTranscript(machineId, runId, transcript, { storage }); + const loadedDb = await loadToolDbFile(machineId, { storage }); + const loadedTranscript = await loadToolDbTranscript(machineId, runId, { storage }); + const diagnostics = createToolDbProcessDiagnostics({ + dbProgramPath, + opfsDbPath: savedDb.displayPath, + transcript, + dbFileText: loadedDb.text, + startupToolCount, + mutationCount, + runtimeMode: startResult.runtimeMode, + runtimeExecutionReady: startResult.runtimeExecutionReady === true && planResult.runtimeExecutionReady === true, + }); + + return { + apiName: "linuxcnc-tool-db-process-runtime-persistence-session", + ready: validation.ready && + diagnostics.runtimeExecutionReady === true && + diagnostics.opfsPersistenceReady === true && + diagnostics.protocolTranscriptReady === true, + startResult, + planResult, + validation, + savedDb, + savedTranscript, + loadedDb, + loadedTranscript, + diagnostics, + executionEnabled: false, + promotionAllowed: false, + }; +} diff --git a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs index e2afb21..8cf02cf 100644 --- a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs +++ b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs @@ -93,6 +93,7 @@ import { createLinuxCncToolDbProcessPort, createToolDbProcessDiagnostics, createToolDbTransactionPlan, + runToolDbProcessPortPersistenceSession, createVirtualHalBridgeActionPlan, createVirtualHalBridgeReadiness, createVirtualHalCommandScriptFixtureReport, @@ -524,6 +525,7 @@ const requiredExports = [ ["createLinuxCncToolDbProcessPort", createLinuxCncToolDbProcessPort], ["createToolDbProcessDiagnostics", createToolDbProcessDiagnostics], ["createToolDbTransactionPlan", createToolDbTransactionPlan], + ["runToolDbProcessPortPersistenceSession", runToolDbProcessPortPersistenceSession], ["validateToolDbTranscript", validateToolDbTranscript], ["saveToolDbFile", saveToolDbFile], ["loadToolDbFile", loadToolDbFile], diff --git a/wasm-port/tests/sdk/node/verify_tool_db_node_runtime_adapter.mjs b/wasm-port/tests/sdk/node/verify_tool_db_node_runtime_adapter.mjs index 9c3ee7e..203b06d 100644 --- a/wasm-port/tests/sdk/node/verify_tool_db_node_runtime_adapter.mjs +++ b/wasm-port/tests/sdk/node/verify_tool_db_node_runtime_adapter.mjs @@ -4,12 +4,64 @@ import { resolve } from "node:path"; import { createLinuxCncToolDbProcessPort, - createToolDbProcessDiagnostics, validateToolDbTranscript, } from "../../../runtime/sdk/src/tool-db-process-port.js"; import { createLinuxCncToolDbNodeRuntimeAdapter, } from "../../../runtime/sdk/src/tool-db-node-runtime-adapter.js"; +import { + runToolDbProcessPortPersistenceSession, +} from "../../../runtime/sdk/src/tool-db-runtime-session.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 adapter = createLinuxCncToolDbNodeRuntimeAdapter({ dbSavefile: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-adapter-db-nonran"), @@ -59,22 +111,42 @@ const dbFileText = readFileSync(adapter.dbSavefile, "utf8"); assert.match(dbFileText, /T11 P111 D0.33 Z0.11/); assert.match(dbFileText, /T14 P114\b/); -const diagnostics = createToolDbProcessDiagnostics({ - dbProgramPath: "./db_nonran.py", - opfsDbPath: "/machines/db-demo/tool-db/db_nonran_file", - transcript, - dbFileText, - startupToolCount: 10, - mutationCount: 3, - runtimeMode: adapter.runtimeMode, - runtimeExecutionReady: true, +const storageRoot = new MockDirectoryHandle(); +const storage = { getDirectory: async () => storageRoot }; +const sessionAdapter = createLinuxCncToolDbNodeRuntimeAdapter({ + dbSavefile: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-persistence-db-nonran"), + stdoutLog: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-persistence.stdout.log"), + stderrLog: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-persistence.stderr.log"), }); -assert.equal(diagnostics.runtimeMode, "node-linuxcnc-db-program"); -assert.equal(diagnostics.runtimeExecutionReady, true); -assert.equal(diagnostics.protocolTranscriptReady, true); -assert.equal(diagnostics.opfsPersistenceReady, true); -assert.equal(diagnostics.tblFallbackSufficient, false); -assert.equal(diagnostics.executionEnabled, false); -assert.equal(diagnostics.promotionAllowed, false); +const session = await runToolDbProcessPortPersistenceSession({ + 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: sessionAdapter.runtimeMode, + runtimeAdapter: sessionAdapter, + }), + machineId: "db-demo", + runId: "node-runtime-session", + dbProgramPath: "./db_nonran.py", + readDbFileText: () => sessionAdapter.readDbFileText(), + storage, +}); +assert.equal(session.apiName, "linuxcnc-tool-db-process-runtime-persistence-session"); +assert.equal(session.ready, true); +assert.equal(session.diagnostics.runtimeMode, "node-linuxcnc-db-program"); +assert.equal(session.diagnostics.runtimeExecutionReady, true); +assert.equal(session.diagnostics.protocolTranscriptReady, true); +assert.equal(session.diagnostics.opfsPersistenceReady, true); +assert.equal(session.diagnostics.tblFallbackSufficient, false); +assert.equal(session.diagnostics.executionEnabled, false); +assert.equal(session.diagnostics.promotionAllowed, false); +assert.equal(session.savedDb.displayPath, "/machines/db-demo/tool-db/db_nonran_file"); +assert.equal(session.savedTranscript.displayPath, "/machines/db-demo/tool-db/transcripts/node-runtime-session/transcript.json"); +assert.match(session.loadedDb.text, /T11 P111 D0.33 Z0.11/); +assert.match(session.loadedTranscript.payload.transcript[0].line, /^v2\.1$/); console.log("tool_db_node_runtime_adapter=ok");