Files
linuxcncWebWASM/web/worker/test_contracts.mjs

79 lines
3.9 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
import { createWorkspacePackage, migrateWorkspacePackage } from "../workspace/migration.mjs";
import {
SCHEMAS,
validateCommandEnvelope,
validateEventLog,
validateSimTask,
validateSnapshot,
validateWorkspacePackage,
} from "./contracts.mjs";
const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
const core = await createModule({ wasmBinary });
const sessionId = "contract-session";
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: sessionId };
function command(kind, seq, payload = {}, operation_id = undefined) {
return { ...base, kind, seq, ...(operation_id ? { operation_id } : {}), payload };
}
validateCommandEnvelope(command("HELLO", 1), { contextId: CONTEXT_ID, sessionId });
validateCommandEnvelope({ ...command("PING", 2), sent_at_monotonic_ms: 12.5 });
validateCommandEnvelope(command("X".repeat(128), 2));
for (const invalid of [
{ ...command("PING", 2), schema_version: 2 },
{ ...command("PING", 2), seq: 0 },
{ ...command("PING", 2), payload: [] },
{ ...command("PING", 2), payload: { value: Number.NaN } },
{ ...command("PING", 2), unknown: true },
command("SIM_STEP", 2, { cycles: 1 }),
command("X".repeat(129), 2),
]) {
assert.throws(() => validateCommandEnvelope(invalid), /^Error: contract_/);
}
assert.throws(() => validateCommandEnvelope(command("PING", 2), { contextId: "other" }), /contract_command_context_mismatch/);
const taskUnsigned = {
schema: SCHEMAS.task,
context_id: CONTEXT_ID,
session_id: sessionId,
state: "READY",
mode: "MANUAL",
program_status: "idle",
seq: 2,
simulation_tick: 0,
current_line: null,
};
validateSimTask({ ...taskUnsigned, state_hash: fnv1a64(taskUnsigned) }, { contextId: CONTEXT_ID, sessionId });
assert.throws(() => validateSimTask({ ...taskUnsigned, state_hash: "fnv1a64:0000000000000000" }), /contract_task_hash/);
assert.throws(() => validateSimTask({ ...taskUnsigned, state: "UNKNOWN", state_hash: fnv1a64(taskUnsigned) }), /contract_task_state/);
const session = new SimSession(core, { sessionId });
assert.equal(session.accept(command("HELLO", 1)).kind, "CAPABILITIES");
assert.equal(session.accept(command("SESSION_ATTACH", 2, {}, "attach")).kind, "SESSION_READY");
assert.equal(session.accept(command("LOAD_WORKSPACE", 3, { root_path: "/workspace" }, "load")).payload.status, "verified");
const stepped = session.accept(command("SIM_STEP", 4, { cycles: 5 }, "step"));
assert.equal(stepped.simulation_tick, 5);
validateSnapshot(stepped.payload.snapshot);
const eventLog = session.timeline.events.export();
validateEventLog(eventLog);
assert.equal(core._sim_protocol_last_seq(), eventLog.cursor);
assert.equal(core._sim_event_count(), eventLog.events.length);
assert.equal(core._sim_protocol_accept(2, 5, "PING", "", 5), -100);
const timeline = new ReplayTimeline(session.timeline.export());
const workspace = createWorkspacePackage({ contextId: CONTEXT_ID, timeline, committedSeq: eventLog.cursor });
validateWorkspacePackage(workspace, { contextId: CONTEXT_ID });
const migrated = migrateWorkspacePackage(workspace, 2);
validateWorkspacePackage(migrated, { contextId: CONTEXT_ID });
assert.throws(() => validateWorkspacePackage({ ...workspace, package_hash: "fnv1a64:0000000000000000" }), /contract_workspace_package_hash/);
assert.throws(() => validateWorkspacePackage({ ...workspace, committed_seq: Number.POSITIVE_INFINITY }), /contract_workspace_committed_seq/);
assert.equal(migrateWorkspacePackage(migrated, 1).package_hash, workspace.package_hash);
console.log(`contracts=passed schemas=${Object.values(SCHEMAS).join(",")} tick=${stepped.simulation_tick} cursor=${eventLog.cursor}`);