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}`);