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 { createMachineSessionSnapshotPayload, createSessionSnapshot, loadMachineSessionSnapshot, loadSessionSnapshot, saveMachineSessionSnapshot, saveSessionSnapshot, validateSessionSnapshot, } from "../../../runtime/opfs/snapshot-store.js"; import { gcodeFilenameFromProgramPath, loadGcodeProgram, loadMachineTextFiles, machineFilePaths, saveGcodeProgram, saveMachineTextFiles, } from "../../../runtime/opfs/machine-file-store.js"; import { restoreMachineParametersFromOpfs, saveMachineParametersToOpfs, } from "../../../runtime/opfs/linuxcnc-parameter-bridge.js"; import { loadMachineToolTableFromOpfs, saveMachineToolTableToOpfs, } from "../../../runtime/opfs/linuxcnc-tool-table-bridge.js"; import { loadMachineSessionFromOpfs, } from "../../../runtime/opfs/linuxcnc-machine-session-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, }; function assertGcodeProgramStoragePath(filename, path) { assert.equal( path, `linuxcnc/gcode/${filename}`, `${filename} G-code OPFS path drift`, ); } function assertSessionSnapshotStoragePath(sessionId, path, filename = "snapshot.json") { assert.equal( path, `linuxcnc/sessions/${sessionId}/${filename}`, `${sessionId}/${filename} session snapshot OPFS path drift`, ); } function assertPreviewCacheStoragePath(cacheKey, path, filename = "preview.json") { assert.equal( path, `linuxcnc/preview-cache/${cacheKey}/${filename}`, `${cacheKey}/${filename} preview cache OPFS path drift`, ); } function assertMachineFileStoragePaths(machineId, paths) { assert.deepEqual( paths, { ini: `linuxcnc/machines/${machineId}/machine.ini`, toolTable: `linuxcnc/machines/${machineId}/tool.tbl`, parameters: `linuxcnc/machines/${machineId}/linuxcnc.var`, }, `${machineId} machine file OPFS path drift`, ); } function assertParameterBridgePaths( machineId, result, wasmPath, filename = "linuxcnc.var", ) { assert.equal( result.opfsPath, `linuxcnc/machines/${machineId}/${filename}`, `${machineId}/${filename} parameter bridge OPFS path drift`, ); assert.equal( result.wasmPath, wasmPath, `${machineId}/${filename} parameter bridge WASM path drift`, ); } function assertToolTableBridgePaths( machineId, result, wasmPath, filename = "tool.tbl", ) { assert.equal( result.opfsPath, `linuxcnc/machines/${machineId}/${filename}`, `${machineId}/${filename} tool table bridge OPFS path drift`, ); assert.equal( result.wasmPath, wasmPath, `${machineId}/${filename} tool table bridge WASM path drift`, ); } function assertSessionBridgePaths( machineId, result, { iniWasmPath = "/work/machine.ini", parameterWasmPath = "/work/linuxcnc.var", toolTableWasmPath = "/work/tool.tbl", } = {}, ) { assert.equal(result.machineId, machineId, `${machineId} session machine id drift`); assert.equal( result.ini.opfsPath, `linuxcnc/machines/${machineId}/machine.ini`, `${machineId} session INI OPFS path drift`, ); assert.equal(result.ini.wasmPath, iniWasmPath, `${machineId} session INI WASM path drift`); assertParameterBridgePaths(machineId, result.parameters, parameterWasmPath); assertToolTableBridgePaths(machineId, result.toolTable, toolTableWasmPath); } 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"); assertGcodeProgramStoragePath("fixture.ngc", gcodeProgramPath("fixture.ngc")); assertGcodeProgramStoragePath( "custom-fixture.ngc", gcodeProgramPath("custom-fixture.ngc"), ); assert.equal( gcodeFilenameFromProgramPath("linuxcnc/gcode/fixture.ngc"), "fixture.ngc", ); assert.throws( () => gcodeFilenameFromProgramPath("linuxcnc/machines/xyzab-tdr/fixture.ngc"), /Invalid G-code program OPFS path/, ); assert.throws( () => gcodeFilenameFromProgramPath("linuxcnc/gcode/nested/fixture.ngc"), /Invalid G-code program OPFS path/, ); assertPreviewCacheStoragePath("fixture", previewCachePath("fixture")); assertPreviewCacheStoragePath( "fixture", previewCachePath("fixture", "custom-preview.json"), "custom-preview.json", ); assertSessionSnapshotStoragePath( "session-1", sessionSnapshotPath("session-1"), ); assertSessionSnapshotStoragePath( "session-1", sessionSnapshotPath("session-1", "custom-snapshot.json"), "custom-snapshot.json", ); assertMachineFileStoragePaths("xyzab-tdr", defaultMachinePaths("xyzab-tdr")); assertMachineFileStoragePaths("xyzab-tdr", machineFilePaths("xyzab-tdr")); const snapshotPayload = createMachineSessionSnapshotPayload("xyzab-tdr", { gcodeFilename: "fixture.ngc", }); assert.deepEqual(snapshotPayload, { machineId: "xyzab-tdr", files: { ini: machineIniPath("xyzab-tdr"), parameters: parameterFilePath("xyzab-tdr"), toolTable: toolTablePath("xyzab-tdr"), gcode: gcodeProgramPath("fixture.ngc"), }, }); assert.deepEqual( createMachineSessionSnapshotPayload("custom-machine", { parameterFilename: "custom.var", toolTableFilename: "custom-tool.tbl", gcodeOpfsPath: "linuxcnc/gcode/custom-fixture.ngc", }), { machineId: "custom-machine", files: { ini: "linuxcnc/machines/custom-machine/machine.ini", parameters: "linuxcnc/machines/custom-machine/custom.var", toolTable: "linuxcnc/machines/custom-machine/custom-tool.tbl", gcode: "linuxcnc/gcode/custom-fixture.ngc", }, }, ); assert.throws( () => createMachineSessionSnapshotPayload("bad-machine", { gcodeFilename: "../escape.ngc" }), /Invalid G-code filename/, ); 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); assert.throws( () => validateSessionSnapshot({ ...snapshot, format: "other-format" }, "session-1"), /Unsupported session snapshot format/, ); assert.throws( () => validateSessionSnapshot({ ...snapshot, version: 99 }, "session-1"), /Unsupported session snapshot version/, ); assert.throws( () => validateSessionSnapshot({ ...snapshot, sessionId: "other-session" }, "session-1"), /Session snapshot id mismatch/, ); assert.throws( () => validateSessionSnapshot({ ...snapshot, metadata: null }, "session-1"), /snapshot metadata must be a plain object/, ); assert.throws( () => validateSessionSnapshot({ ...snapshot, payload: [] }, "session-1"), /snapshot payload must be a plain object/, ); 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" }, }); assertSessionSnapshotStoragePath( "session-1", sessionSnapshotPath("session-1"), ); assert.deepEqual(await loadSessionSnapshot("session-1", { storage }), snapshot); const customSnapshot = await saveSessionSnapshot("session-1", snapshotPayload, { storage, filename: "custom-snapshot.json", createdAt: "2026-06-08T00:00:00.000Z", metadata: { source: "node-smoke-custom" }, }); assertSessionSnapshotStoragePath( "session-1", sessionSnapshotPath("session-1", "custom-snapshot.json"), "custom-snapshot.json", ); assert.equal( customSnapshot.metadata.source, "node-smoke-custom", ); assert.deepEqual( await loadSessionSnapshot("session-1", { storage, filename: "custom-snapshot.json", }), customSnapshot, ); const machineSessionSnapshot = await saveMachineSessionSnapshot( "machine-session-1", "xyzab-tdr", { storage, filename: "machine-session-snapshot.json", gcodeFilename: "fixture.ngc", createdAt: "2026-06-08T00:00:00.000Z", metadata: { source: "node-machine-session" }, }, ); assertSessionSnapshotStoragePath( "machine-session-1", sessionSnapshotPath("machine-session-1", "machine-session-snapshot.json"), "machine-session-snapshot.json", ); assert.deepEqual(machineSessionSnapshot.payload, snapshotPayload); assert.deepEqual( await loadSessionSnapshot("machine-session-1", { storage, filename: "machine-session-snapshot.json", }), machineSessionSnapshot, ); assert.deepEqual( await loadMachineSessionSnapshot("machine-session-1", "xyzab-tdr", { storage, filename: "machine-session-snapshot.json", }), machineSessionSnapshot, ); await assert.rejects( () => loadMachineSessionSnapshot("machine-session-1", "other-machine", { storage, filename: "machine-session-snapshot.json", }), /Machine session snapshot id mismatch/, ); await saveTextFile( sessionSnapshotPath("machine-session-bad-files"), JSON.stringify({ ...machineSessionSnapshot, sessionId: "machine-session-bad-files", payload: { machineId: "xyzab-tdr", files: { ini: "linuxcnc/machines/xyzab-tdr/machine.ini", parameters: "../linuxcnc.var", toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl", }, }, }), storage, ); await assert.rejects( () => loadMachineSessionSnapshot("machine-session-bad-files", "xyzab-tdr", { storage }), /Invalid OPFS path/, ); 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", }); assertGcodeProgramStoragePath( "fixture.ngc", await saveGcodeProgram("fixture.ngc", "G0 X0 Y0\nM2\n", { storage }), ); assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n"); assertGcodeProgramStoragePath( "custom-fixture.ngc", await saveGcodeProgram("custom-fixture.ngc", "G1 X1 F10\nM2\n", { storage }), ); assert.equal(await loadGcodeProgram("custom-fixture.ngc", { storage }), "G1 X1 F10\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`; }, loadToolTable(path, options = {}) { return [ "tooldata_load=0", `load_tool_path=${path}`, `random_toolchanger=${options.randomToolChanger === true ? 1 : 0}`, "", ].join("\n"); }, saveToolTable(path) { bridgeFiles.set(path, "T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n"); return `tooldata_save=0\nsave_tool_path=${path}\n`; }, }; const bridgeIniSdk = { writeTextFile(path, text) { bridgeFiles.set(`ini:${path}`, text); }, getString(path, section, tag) { if ( path === "/work/ini-file-session.ini" && section === "RS274NGC" && tag === "PARAMETER_FILE" ) { return "custom.var"; } if ( path === "/work/ini-override-session.ini" && section === "RS274NGC" && tag === "PARAMETER_FILE" ) { return "ignored.var"; } if ( path === "/work/invalid-ini-file-session.ini" && section === "RS274NGC" && tag === "PARAMETER_FILE" ) { return "../escape.var"; } if ( path === "/work/ini-file-session.ini" && section === "EMCIO" && tag === "TOOL_TABLE" ) { return "custom-tool.tbl"; } if ( path === "/work/ini-override-session.ini" && section === "EMCIO" && tag === "TOOL_TABLE" ) { return "ignored-tool.tbl"; } if ( path === "/work/invalid-tool-file-session.ini" && section === "EMCIO" && tag === "TOOL_TABLE" ) { return "nested/tool.tbl"; } return null; }, getBool(path, section, tag) { if ( path === "/work/ini-derived-session.ini" && section === "EMCIO" && tag === "RANDOM_TOOLCHANGER" ) { return true; } return null; }, }; 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"); assertParameterBridgePaths("xyzab-tdr", restoredParameters, "/work/bridge.var"); const restoredDefaultParameters = await restoreMachineParametersFromOpfs( bridgeInterp, "xyzab-tdr", { storage }, ); assertParameterBridgePaths("xyzab-tdr", restoredDefaultParameters, "/work/linuxcnc.var"); assert.equal( restoredDefaultParameters.result, "restore_parameters=0\nrestore_path=/work/linuxcnc.var\n", ); assert.equal(bridgeFiles.get("/work/linuxcnc.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" }, ); assertParameterBridgePaths("xyzab-tdr", savedParameters, "/work/bridge.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", ); const savedDefaultParameters = await saveMachineParametersToOpfs( bridgeInterp, "xyzab-tdr", { 5161: 12.34 }, { storage }, ); assertParameterBridgePaths("xyzab-tdr", savedDefaultParameters, "/work/linuxcnc.var"); assert.equal( savedDefaultParameters.backupOpfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak", ); assert.equal(savedDefaultParameters.result.includes("save_path=/work/linuxcnc.var"), true); const loadedToolTable = await loadMachineToolTableFromOpfs( bridgeInterp, "xyzab-tdr", { storage, wasmPath: "/work/bridge-tool.tbl" }, ); assert.deepEqual(loadedToolTable, { opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl", wasmPath: "/work/bridge-tool.tbl", result: "tooldata_load=0\nload_tool_path=/work/bridge-tool.tbl\nrandom_toolchanger=0\n", }); assert.equal(bridgeFiles.get("/work/bridge-tool.tbl"), "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n"); assertToolTableBridgePaths("xyzab-tdr", loadedToolTable, "/work/bridge-tool.tbl"); const loadedDefaultToolTable = await loadMachineToolTableFromOpfs( bridgeInterp, "xyzab-tdr", { storage }, ); assertToolTableBridgePaths("xyzab-tdr", loadedDefaultToolTable, "/work/tool.tbl"); assert.equal( loadedDefaultToolTable.result, "tooldata_load=0\nload_tool_path=/work/tool.tbl\nrandom_toolchanger=0\n", ); assert.equal(bridgeFiles.get("/work/tool.tbl"), "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n"); const loadedRandomToolTable = await loadMachineToolTableFromOpfs( bridgeInterp, "xyzab-tdr", { storage, wasmPath: "/work/random-bridge-tool.tbl", randomToolChanger: true }, ); assert.deepEqual(loadedRandomToolTable, { opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl", wasmPath: "/work/random-bridge-tool.tbl", result: "tooldata_load=0\nload_tool_path=/work/random-bridge-tool.tbl\nrandom_toolchanger=1\n", }); const savedToolTable = await saveMachineToolTableToOpfs( bridgeInterp, "xyzab-tdr", { storage, wasmPath: "/work/bridge-tool.tbl" }, ); assert.equal(savedToolTable.opfsPath, "linuxcnc/machines/xyzab-tdr/tool.tbl"); assert.equal(savedToolTable.wasmPath, "/work/bridge-tool.tbl"); assertToolTableBridgePaths("xyzab-tdr", savedToolTable, "/work/bridge-tool.tbl"); assert.equal(savedToolTable.result.includes("tooldata_save=0"), true); assert.equal( await loadTextFile("linuxcnc/machines/xyzab-tdr/tool.tbl", storage), "T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n", ); const savedDefaultToolTable = await saveMachineToolTableToOpfs( bridgeInterp, "xyzab-tdr", { storage }, ); assertToolTableBridgePaths("xyzab-tdr", savedDefaultToolTable, "/work/tool.tbl"); assert.equal(savedDefaultToolTable.result.includes("save_tool_path=/work/tool.tbl"), true); const loadedSession = await loadMachineSessionFromOpfs( bridgeInterp, "xyzab-tdr", { storage, iniWasmPath: "/work/session.ini", parameterWasmPath: "/work/session.var", toolTableWasmPath: "/work/session-tool.tbl", }, ); assert.deepEqual(loadedSession, { machineId: "xyzab-tdr", ini: { opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini", wasmPath: "/work/session.ini", }, parameters: { opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var", wasmPath: "/work/session.var", result: "restore_parameters=0\nrestore_path=/work/session.var\n", }, toolTable: { opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl", wasmPath: "/work/session-tool.tbl", result: "tooldata_load=0\nload_tool_path=/work/session-tool.tbl\nrandom_toolchanger=0\n", }, }); assertSessionBridgePaths("xyzab-tdr", loadedSession, { iniWasmPath: "/work/session.ini", parameterWasmPath: "/work/session.var", toolTableWasmPath: "/work/session-tool.tbl", }); assert.equal(bridgeFiles.get("/work/session.ini"), "[EMC]\nMACHINE = xyzab-tdr\n"); assert.equal(bridgeFiles.get("/work/session.var"), "5161\t12.340000\n5162\t56.780000\n"); assert.equal( bridgeFiles.get("/work/session-tool.tbl"), "T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n", ); const loadedDefaultSession = await loadMachineSessionFromOpfs( bridgeInterp, "xyzab-tdr", { storage }, ); assertSessionBridgePaths("xyzab-tdr", loadedDefaultSession); assert.equal(bridgeFiles.get("/work/machine.ini"), "[EMC]\nMACHINE = xyzab-tdr\n"); assert.equal(bridgeFiles.get("/work/linuxcnc.var"), "5161\t12.340000\n5162\t56.780000\n"); assert.equal( bridgeFiles.get("/work/tool.tbl"), "T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n", ); const loadedRandomSession = await loadMachineSessionFromOpfs( bridgeInterp, "xyzab-tdr", { storage, iniWasmPath: "/work/random-session.ini", parameterWasmPath: "/work/random-session.var", toolTableWasmPath: "/work/random-session-tool.tbl", randomToolChanger: true, }, ); assert.equal( loadedRandomSession.toolTable.result, "tooldata_load=0\nload_tool_path=/work/random-session-tool.tbl\nrandom_toolchanger=1\n", ); const loadedIniDerivedRandomSession = await loadMachineSessionFromOpfs( bridgeInterp, "xyzab-tdr", { storage, iniSdk: bridgeIniSdk, iniWasmPath: "/work/ini-derived-session.ini", parameterWasmPath: "/work/ini-derived-session.var", toolTableWasmPath: "/work/ini-derived-session-tool.tbl", }, ); assert.equal( bridgeFiles.get("ini:/work/ini-derived-session.ini"), "[EMC]\nMACHINE = xyzab-tdr\n", ); assert.equal( loadedIniDerivedRandomSession.toolTable.result, "tooldata_load=0\nload_tool_path=/work/ini-derived-session-tool.tbl\nrandom_toolchanger=1\n", ); assert.equal( loadedIniDerivedRandomSession.parameters.opfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var", ); assert.equal( loadedIniDerivedRandomSession.toolTable.opfsPath, "linuxcnc/machines/xyzab-tdr/tool.tbl", ); await saveMachineTextFiles("ini-file-session", { ini: [ "[EMC]", "MACHINE = ini-file-session", "", "[RS274NGC]", "PARAMETER_FILE = custom.var", "", "[EMCIO]", "TOOL_TABLE = custom-tool.tbl", "", ].join("\n"), }, { storage }); await saveTextFile( "linuxcnc/machines/ini-file-session/custom.var", "5161 77.0\n5162 88.0\n", storage, ); await saveTextFile( "linuxcnc/machines/ini-file-session/custom-tool.tbl", "T8 P8 Z4.5 D0.5\n", storage, ); const loadedIniFileSession = await loadMachineSessionFromOpfs( bridgeInterp, "ini-file-session", { storage, iniSdk: bridgeIniSdk, iniWasmPath: "/work/ini-file-session.ini", parameterWasmPath: "/work/ini-file-session.var", toolTableWasmPath: "/work/ini-file-session-tool.tbl", }, ); assert.equal( loadedIniFileSession.parameters.opfsPath, "linuxcnc/machines/ini-file-session/custom.var", ); assert.equal( loadedIniFileSession.toolTable.opfsPath, "linuxcnc/machines/ini-file-session/custom-tool.tbl", ); assert.equal(bridgeFiles.get("/work/ini-file-session.var"), "5161 77.0\n5162 88.0\n"); assert.equal(bridgeFiles.get("/work/ini-file-session-tool.tbl"), "T8 P8 Z4.5 D0.5\n"); assert.equal( loadedIniFileSession.toolTable.result, "tooldata_load=0\nload_tool_path=/work/ini-file-session-tool.tbl\nrandom_toolchanger=0\n", ); await saveMachineTextFiles("ini-override-session", { ini: [ "[EMC]", "MACHINE = ini-override-session", "", "[RS274NGC]", "PARAMETER_FILE = ignored.var", "", "[EMCIO]", "TOOL_TABLE = ignored-tool.tbl", "", ].join("\n"), }, { storage }); await saveTextFile( "linuxcnc/machines/ini-override-session/explicit.var", "5161 11.0\n5162 22.0\n", storage, ); await saveTextFile( "linuxcnc/machines/ini-override-session/explicit-tool.tbl", "T3 P3 Z1.75 D0.375\n", storage, ); const loadedIniOverrideSession = await loadMachineSessionFromOpfs( bridgeInterp, "ini-override-session", { storage, iniSdk: bridgeIniSdk, iniWasmPath: "/work/ini-override-session.ini", parameterFilename: "explicit.var", parameterWasmPath: "/work/ini-override-session.var", toolTableFilename: "explicit-tool.tbl", toolTableWasmPath: "/work/ini-override-session-tool.tbl", }, ); assert.equal( loadedIniOverrideSession.parameters.opfsPath, "linuxcnc/machines/ini-override-session/explicit.var", ); assert.equal( loadedIniOverrideSession.toolTable.opfsPath, "linuxcnc/machines/ini-override-session/explicit-tool.tbl", ); assert.equal(bridgeFiles.get("/work/ini-override-session.var"), "5161 11.0\n5162 22.0\n"); assert.equal(bridgeFiles.get("/work/ini-override-session-tool.tbl"), "T3 P3 Z1.75 D0.375\n"); await saveMachineTextFiles("invalid-ini-file-session", { ini: [ "[EMC]", "MACHINE = invalid-ini-file-session", "", "[RS274NGC]", "PARAMETER_FILE = ../escape.var", "", ].join("\n"), }, { storage }); await assert.rejects( () => loadMachineSessionFromOpfs( bridgeInterp, "invalid-ini-file-session", { storage, iniSdk: bridgeIniSdk, iniWasmPath: "/work/invalid-ini-file-session.ini", }, ), /Invalid parameter filename/, ); await saveMachineTextFiles("invalid-tool-file-session", { ini: [ "[EMC]", "MACHINE = invalid-tool-file-session", "", "[EMCIO]", "TOOL_TABLE = nested/tool.tbl", "", ].join("\n"), }, { storage }); await assert.rejects( () => loadMachineSessionFromOpfs( bridgeInterp, "invalid-tool-file-session", { storage, iniSdk: bridgeIniSdk, iniWasmPath: "/work/invalid-tool-file-session.ini", }, ), /Invalid tool table filename/, ); 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/, ); assert.throws( () => sessionSnapshotPath("session-1", "nested/snapshot.json"), /Invalid session snapshot filename/, ); assert.throws( () => previewCachePath("fixture", "nested/preview.json"), /Invalid preview cache filename/, ); assert.throws( () => gcodeProgramPath("../escape.ngc"), /Invalid G-code filename/, ); await assert.rejects( () => saveGcodeProgram("nested/fixture.ngc", "", { storage }), /Invalid G-code filename/, ); await assert.rejects( () => loadGcodeProgram("../escape.ngc", { storage }), /Invalid G-code filename/, ); await assert.rejects( () => saveSessionSnapshot("session-1", {}, { storage, filename: "nested/snapshot.json", }), /Invalid session snapshot filename/, ); await assert.rejects( () => loadSessionSnapshot("session-1", { storage, filename: "../snapshot.json", }), /Invalid session snapshot filename/, ); 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 saveTextFile( sessionSnapshotPath("bad-format"), JSON.stringify({ ...snapshot, sessionId: "bad-format", format: "other-format" }), storage, ); await assert.rejects( () => loadSessionSnapshot("bad-format", { storage }), /Unsupported session snapshot format/, ); await saveTextFile( sessionSnapshotPath("bad-version"), JSON.stringify({ ...snapshot, sessionId: "bad-version", version: 99 }), storage, ); await assert.rejects( () => loadSessionSnapshot("bad-version", { storage }), /Unsupported session snapshot version/, ); await saveTextFile( sessionSnapshotPath("bad-session-id"), JSON.stringify({ ...snapshot, sessionId: "other-session" }), storage, ); await assert.rejects( () => loadSessionSnapshot("bad-session-id", { storage }), /Session snapshot id mismatch/, ); 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");