Files
cnc_wams/wasm-port/tests/opfs/node/verify_file_service.mjs

947 lines
27 KiB
JavaScript

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 {
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"),
);
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");