结论:已新增 OPFS machine file store,支持 INI、tool table、parameter file 和 G-code 的纯文本保存/读取,并在 Node mock 与 Chromium smoke 中验证往返,不引入 CNC 文件格式语义。
187 lines
5.1 KiB
JavaScript
187 lines
5.1 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 {
|
|
createSessionSnapshot,
|
|
loadSessionSnapshot,
|
|
saveSessionSnapshot,
|
|
validateSessionSnapshot,
|
|
} from "../../../runtime/opfs/snapshot-store.js";
|
|
import {
|
|
loadGcodeProgram,
|
|
loadMachineTextFiles,
|
|
machineFilePaths,
|
|
saveGcodeProgram,
|
|
saveMachineTextFiles,
|
|
} from "../../../runtime/opfs/machine-file-store.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");
|
|
|
|
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");
|