67 lines
5.1 KiB
JavaScript
67 lines
5.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { handleWorkerMessage } from "./opfs-worker-bridge.mjs";
|
|
import { loadUiState } from "./opfs-ui-state.mjs";
|
|
|
|
class File {
|
|
constructor() { this.bytes = new Uint8Array(); }
|
|
async getFile() { return { text: async () => new TextDecoder().decode(this.bytes) }; }
|
|
async createSyncAccessHandle() {
|
|
const file = this;
|
|
return {
|
|
truncate(size) { file.bytes = file.bytes.slice(0, size); },
|
|
write(bytes, { at = 0 } = {}) { const next = new Uint8Array(Math.max(file.bytes.length, at + bytes.byteLength)); next.set(file.bytes); next.set(bytes, at); file.bytes = next; return bytes.byteLength; },
|
|
flush() {}, close() {},
|
|
};
|
|
}
|
|
}
|
|
|
|
class Directory {
|
|
constructor() { this.files = new Map(); this.directories = new Map(); }
|
|
async getDirectoryHandle(name, { create = false } = {}) { if (!this.directories.has(name)) { if (!create) throw Object.assign(new Error("missing"), { name: "NotFoundError" }); this.directories.set(name, new Directory()); } return this.directories.get(name); }
|
|
async getFileHandle(name, { create = false } = {}) { if (!this.files.has(name)) { if (!create) throw Object.assign(new Error("missing"), { name: "NotFoundError" }); this.files.set(name, new File()); } return this.files.get(name); }
|
|
async removeEntry(name) { this.files.delete(name); this.directories.delete(name); }
|
|
}
|
|
|
|
const storage = { root: new Directory(), async getDirectory() { return this.root; } };
|
|
const previousNavigator = globalThis.navigator;
|
|
Object.defineProperty(globalThis, "navigator", { configurable: true, value: { storage } });
|
|
const contextId = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
|
const base = { schema_version: 1, context_id: contextId, session_id: "bridge-test" };
|
|
let accepted = 0;
|
|
let lastAccepted = null;
|
|
const session = { accept(input) { accepted += 1; lastAccepted = input; return { schema_version: 1, kind: "COMMAND_RESULT", context_id: input.context_id, session_id: input.session_id, seq: input.seq, payload: { status: "verified", command: input.kind, active: input.payload?.active, recovery_checkpoint: input.payload?.checkpoint ?? undefined } }; } };
|
|
|
|
const saved = await handleWorkerMessage(session, { ...base, kind: "UI_STATE_SAVE", seq: 2000000001, operation_id: "ui-save", payload: { key: "xyzac-trt", state: { view: "p", zoom: 1 } } });
|
|
assert.equal(saved.kind, "UI_STATE_RESULT");
|
|
assert.equal(saved.payload.storage, "opfs");
|
|
assert.equal(accepted, 0);
|
|
const loaded = await handleWorkerMessage(session, { ...base, kind: "UI_STATE_LOAD", seq: 2000000002, operation_id: "ui-load", payload: { key: "xyzac-trt" } });
|
|
assert.equal(loaded.payload.state.view, "p");
|
|
assert.equal(accepted, 0);
|
|
|
|
const checkpoint = { schema: "xyzac-power-off-checkpoint.v1", simulation_tick: 3, state: { program: { source_text: "G21\nM2\n" } }, state_hash: "test" };
|
|
const powerOff = await handleWorkerMessage(session, { ...base, kind: "POWER", seq: 1, operation_id: "power-off", payload: { active: false, checkpoint } });
|
|
assert.equal(powerOff.payload.recovery_persistence.storage, "opfs");
|
|
await handleWorkerMessage(session, { ...base, kind: "LOAD_WORKSPACE", seq: 2, operation_id: "load", payload: { root_path: "/workspace" } });
|
|
assert.deepEqual(lastAccepted.payload.recovery_checkpoint, checkpoint);
|
|
assert.equal(accepted, 2);
|
|
assert.equal((await loadUiState({ contextId, key: "xyzac-trt-power-off" })).status, "verified");
|
|
session.machineDefinition = { id: "xyzbc-trt" };
|
|
const bcCheckpoint = { ...checkpoint, state_hash: "bc-test" };
|
|
await handleWorkerMessage(session, { ...base, kind: "POWER", seq: 3, operation_id: "bc-off", payload: { active: false, checkpoint: bcCheckpoint } });
|
|
assert.equal((await loadUiState({ contextId, key: "xyzbc-trt-power-off" })).state.checkpoint.state_hash, "bc-test");
|
|
assert.equal((await loadUiState({ contextId, key: "xyzac-trt-power-off" })).state.checkpoint.state_hash, "test");
|
|
await handleWorkerMessage(session, { ...base, kind: "LOAD_WORKSPACE", seq: 4, operation_id: "reload", payload: { root_path: "/workspace" } });
|
|
assert.equal(lastAccepted.payload.recovery_checkpoint, undefined, "active-session configuration reload must not inject old cached state");
|
|
delete session.machineDefinition;
|
|
session.core = {};
|
|
session.validateRecoveryGeometry = () => { throw new Error("sim_program_checkpoint_build_mismatch"); };
|
|
const incompatible = await handleWorkerMessage(session, { ...base, kind: "LOAD_WORKSPACE", seq: 5, operation_id: "incompatible", payload: { root_path: "/workspace", configuration: { entrypoint: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini" } } });
|
|
assert.equal(lastAccepted.payload.recovery_checkpoint, undefined);
|
|
assert.equal(incompatible.payload.recovery_persistence.status, "ignored");
|
|
assert.equal(incompatible.payload.recovery_persistence.code, "sim_program_checkpoint_build_mismatch");
|
|
|
|
if (previousNavigator === undefined) delete globalThis.navigator;
|
|
else Object.defineProperty(globalThis, "navigator", { configurable: true, value: previousNavigator });
|
|
console.log(JSON.stringify({ schema: "opfs-worker-bridge.v1", status: "verified", ui_commands_do_not_consume_wasm_sequence: true, poweroff_checkpoint_roundtrip: true }));
|