import assert from "node:assert/strict"; import { getOpfsRoot, loadTextFile, saveTextFile, } from "../../../runtime/opfs/file-service.js"; import { defaultMachinePaths, gcodeProgramPath, machineIniPath, normalizeOpfsPath, parameterFilePath, previewCachePath, sessionSnapshotPath, toolTablePath, } from "../../../runtime/opfs/path-model.js"; import { createSessionSnapshot, loadSessionSnapshot, saveSessionSnapshot, validateSessionSnapshot, } from "../../../runtime/opfs/snapshot-store.js"; import { loadGcodeProgram, loadMachineTextFiles, machineFilePaths, saveGcodeProgram, saveMachineTextFiles, } from "../../../runtime/opfs/machine-file-store.js"; import { restoreMachineParametersFromOpfs, saveMachineParametersToOpfs, } from "../../../runtime/opfs/linuxcnc-parameter-bridge.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(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(parameterFilePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/linuxcnc.var"); assert.equal(gcodeProgramPath("fixture.ngc"), "linuxcnc/gcode/fixture.ngc"); assert.equal(previewCachePath("fixture"), "linuxcnc/preview-cache/fixture/preview.json"); assert.equal(sessionSnapshotPath("session-1"), "linuxcnc/sessions/session-1/snapshot.json"); assert.deepEqual(defaultMachinePaths("xyzab-tdr"), { ini: "linuxcnc/machines/xyzab-tdr/machine.ini", toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl", parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var", }); assert.deepEqual(machineFilePaths("xyzab-tdr"), defaultMachinePaths("xyzab-tdr")); const snapshotPayload = { files: { ini: machineIniPath("xyzab-tdr"), toolTable: toolTablePath("xyzab-tdr"), }, }; const snapshot = createSessionSnapshot("session-1", snapshotPayload, { createdAt: "2026-06-08T00:00:00.000Z", metadata: { source: "node-smoke" }, }); assert.deepEqual(snapshot, { format: "linuxcnc-wasm-session-snapshot", version: 1, sessionId: "session-1", createdAt: "2026-06-08T00:00:00.000Z", metadata: { source: "node-smoke" }, payload: snapshotPayload, }); assert.equal(validateSessionSnapshot(snapshot, "session-1"), snapshot); await saveTextFile( "linuxcnc/machines/xyzab.ini", "[EMC]\nMACHINE = opfs-smoke\n", storage, ); assert.equal( await loadTextFile("linuxcnc/machines/xyzab.ini", storage), "[EMC]\nMACHINE = opfs-smoke\n", ); await saveSessionSnapshot("session-1", snapshotPayload, { storage, createdAt: "2026-06-08T00:00:00.000Z", metadata: { source: "node-smoke" }, }); assert.deepEqual(await loadSessionSnapshot("session-1", { storage }), snapshot); await saveMachineTextFiles("xyzab-tdr", { ini: "[EMC]\nMACHINE = xyzab-tdr\n", toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n", parameters: "5161 0.0\n5162 0.0\n", }, { storage }); assert.deepEqual(await loadMachineTextFiles("xyzab-tdr", { storage }), { ini: "[EMC]\nMACHINE = xyzab-tdr\n", toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n", parameters: "5161 0.0\n5162 0.0\n", }); assert.equal( await saveGcodeProgram("fixture.ngc", "G0 X0 Y0\nM2\n", { storage }), "linuxcnc/gcode/fixture.ngc", ); assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n"); const bridgeFiles = new Map(); const bridgeInterp = { writeTextFile(path, text) { bridgeFiles.set(path, text); }, readTextFile(path) { if (!bridgeFiles.has(path)) { throw new Error(`missing wasm file: ${path}`); } return bridgeFiles.get(path); }, restoreParameters(path) { return `restore_parameters=0\nrestore_path=${path}\n`; }, saveParameters(path, values) { bridgeFiles.set(path, "5161\t12.340000\n5162\t56.780000\n"); bridgeFiles.set(`${path}.bak`, "5161 0.0\n5162 0.0\n"); return `save_parameters=0\nsave_path=${path}\nvalues=${Object.keys(values).join(",")}\n`; }, }; const restoredParameters = await restoreMachineParametersFromOpfs( bridgeInterp, "xyzab-tdr", { storage, wasmPath: "/work/bridge.var" }, ); assert.deepEqual(restoredParameters, { opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var", wasmPath: "/work/bridge.var", result: "restore_parameters=0\nrestore_path=/work/bridge.var\n", }); assert.equal(bridgeFiles.get("/work/bridge.var"), "5161 0.0\n5162 0.0\n"); const savedParameters = await saveMachineParametersToOpfs( bridgeInterp, "xyzab-tdr", { 5161: 12.34, 5162: 56.78 }, { storage, wasmPath: "/work/bridge.var" }, ); assert.equal(savedParameters.opfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var"); assert.equal(savedParameters.backupOpfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak"); assert.equal(savedParameters.result.includes("save_parameters=0"), true); assert.equal( await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var", storage), "5161\t12.340000\n5162\t56.780000\n", ); assert.equal( await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak", storage), "5161 0.0\n5162 0.0\n", ); await assert.rejects( () => loadTextFile("linuxcnc/machines/missing.ini", storage), /missing file/, ); await assert.rejects( () => saveTextFile("../escape.ini", "", storage), /Invalid OPFS path/, ); assert.throws( () => machineIniPath("../escape"), /Invalid machine id/, ); await assert.rejects( () => saveMachineTextFiles("xyzab-tdr", null, { storage }), /machine files must be a plain object/, ); await saveTextFile(sessionSnapshotPath("bad-json"), "{", storage); await assert.rejects( () => loadSessionSnapshot("bad-json", { storage }), /Invalid session snapshot JSON/, ); await assert.rejects( () => loadSessionSnapshot("missing-session", { storage }), /missing directory|missing file/, ); await assert.rejects( () => getOpfsRoot({}), /OPFS is not available/, ); console.log("opfs_file_service_node_smoke=ok");