import assert from "node:assert/strict"; import test from "node:test"; import { parse } from "lossless-json"; import { decodeResponse, decodeNotification, decodeValue, formatSnapshot, ProtocolError, Command, } from "../src/protocol"; import { actor, json, REF } from "./fixtures"; const command: Command = { operation: "list_actors" }; const envelope = (reply: unknown) => json({ version: 3, request_id: "test", result: { Ok: reply } }); test("notification envelopes preserve u64 revisions and reject malformed events", () => { const notification = (data: unknown) => json({ version: 3, event: { event: "image_changed", data, }, }); assert.equal( decodeNotification(notification({ revision: 18446744073709551615n })), 18446744073709551615n, ); assert.equal(decodeNotification(envelope({ kind: "actors", data: [] })), undefined); for (const data of [ { revision: 0 }, { revision: -1 }, { revision: 1.5 }, { revision: 18446744073709551616n }, { revision: "1" }, { revision: 1, extra: true }, {}, ]) { assert.throws(() => decodeNotification(notification(data))); } assert.throws(() => decodeNotification( json({ version: 2, event: { event: "image_changed", data: { revision: 1 } }, }), ), ); }); test("v3 snapshots preserve all value variants, i64 limits, u64 telemetry, and mailbox order", () => { const snapshot = actor(); const reply = decodeResponse(envelope({ kind: "actors", data: [snapshot] }), "test", command); assert.deepEqual(reply, { kind: "actors", data: [snapshot] }); const formatted = formatSnapshot(snapshot, "[::1]:3737", "2026-11-03T00:00:00Z", false); assert.match(formatted, /9223372036854775807/); assert.match(formatted, /-9223372036854775808/); assert.match(formatted, /18446744073709551615/); assert.deepEqual(parse(formatted, undefined, BigInt), { connection: { endpoint: "[::1]:3737", connected: false }, captured_at: "2026-11-03T00:00:00Z", actor: snapshot, }); }); test("version, correlation, result, reply kind, and actor identity are checked", () => { for (const change of [ { version: 2 }, { request_id: "wrong" }, { result: { Ok: { kind: "actor", data: actor() } } }, { result: { Ok: {}, Err: {} } }, { result: {} }, ]) { assert.throws(() => decodeResponse( json({ version: 3, request_id: "test", result: { Ok: { kind: "actors", data: [] } }, ...change, }), "test", command, ), ); } assert.throws(() => decodeResponse(envelope({ kind: "actors", data: [actor(), actor()] }), "test", command), ); assert.throws(() => decodeResponse(envelope({ kind: "actor", data: actor() }), "test", { operation: "inspect_actor", parameters: { actor_ref: "ABCDEFGHIJKLMNOPQRSTU" }, }), ); }); test("server errors are explicit, typed, and not treated as empty actor lists", () => { assert.throws( () => decodeResponse( json({ version: 3, request_id: "test", result: { Err: { code: "not_found", message: "No actor" } }, }), "test", command, ), (error) => error instanceof ProtocolError && error.code === "not_found", ); }); test("mutation replies must match the requested operation and carry validated data", () => { const spawn: Command = { operation: "spawn", parameters: { behavior_name: "counter", state: { type: "integer", data: 0n }, mailbox_capacity: 64, }, }; assert.deepEqual( decodeResponse(envelope({ kind: "actor_spawned", data: { actor_ref: REF } }), "test", spawn), { kind: "actor_spawned", data: { actor_ref: REF } }, ); assert.throws(() => decodeResponse(envelope({ kind: "actor_spawned", data: { actor_ref: "bad" } }), "test", spawn), ); assert.throws(() => decodeResponse(envelope({ kind: "acknowledged" }), "test", spawn)); const tick: Command = { operation: "tick", parameters: { ticks: 10, instruction_budget: 32 } }; assert.deepEqual( decodeResponse(envelope({ kind: "tick_completed", data: { ticks: 10 } }), "test", tick), { kind: "tick_completed", data: { ticks: 10n } }, ); assert.throws(() => decodeResponse(envelope({ kind: "tick_completed", data: { ticks: 11 } }), "test", tick), ); for (const operation of [ "stop_actor", "restart_actor", "pause_actor", "resume_actor", "send", ] as const) { const command: Command = operation === "send" ? { operation, parameters: { actor_ref: REF, message: { type: "null" } } } : { operation, parameters: { actor_ref: REF } }; assert.deepEqual(decodeResponse(envelope({ kind: "acknowledged" }), "test", command), { kind: "acknowledged", }); assert.throws(() => decodeResponse(envelope({ kind: "actors", data: [] }), "test", command)); } }); test("behavior listings validate names, revisions, history, and uniqueness", () => { const behavior = { name: "counter", revision: 1n, current_implementation_id: "id", history: [{ revision: 1n, implementation_id: "id" }], }; assert.deepEqual( decodeResponse(envelope({ kind: "behaviors", data: [behavior] }), "test", { operation: "get_behaviors", }), { kind: "behaviors", data: [behavior] }, ); for (const data of [ [{ ...behavior, name: "" }], [{ ...behavior, revision: 0 }], [{ ...behavior, history: [{ revision: 0, implementation_id: "id" }] }], [behavior, behavior], ]) assert.throws(() => decodeResponse(envelope({ kind: "behaviors", data }), "test", { operation: "get_behaviors" }), ); }); test("malformed states and snapshots fail instead of being coerced", () => { for (const state of [ { type: "integer", data: 9223372036854775808n }, { type: "integer", data: -9223372036854775809n }, { type: "integer", data: 1.5 }, { type: "actor_ref", data: "guessable" }, { type: "string", data: REF, extra: true }, { type: "null", data: null }, { type: "float", data: 1 }, ]) { assert.throws(() => decodeResponse(envelope({ kind: "actors", data: [{ ...actor(), state }] }), "test", command), ); } for (const change of [ { status: { status: "unknown" } }, { status: { status: "failed" } }, { incarnation: -1 }, { mailbox_id: 18446744073709551616n }, { execution: { vm_steps: "20" } }, { reserved: undefined }, { queued: null }, { actor_ref: "@invalid" }, ]) { assert.throws(() => decodeResponse( envelope({ kind: "actors", data: [{ ...actor(), ...change }] }), "test", command, ), ); } }); test("portable value limits have inclusive depth, node, and UTF-8 byte boundaries", () => { let nested: unknown = { type: "null" }; for (let i = 0; i < 32; i++) nested = { type: "list", data: [nested] }; assert.doesNotThrow(() => decodeValue(nested)); assert.throws(() => decodeValue({ type: "list", data: [nested] }), /depth/); const data = Array.from({ length: 4095 }, () => ({ type: "null" })); assert.doesNotThrow(() => decodeValue({ type: "list", data })); assert.throws(() => decodeValue({ type: "list", data: [...data, { type: "null" }] }), /node/); assert.doesNotThrow(() => decodeValue({ type: "string", data: "é".repeat(524288) })); assert.throws(() => decodeValue({ type: "string", data: "é".repeat(524288) + "a" }), /byte/); });