import assert from "node:assert/strict"; import test from "node:test"; import { parse } from "lossless-json"; import { decodeResponse, decodeNotification, decodeValue, formatSnapshot, isSourcePath, isSourceRevision, ProtocolError, Command, formatLogEntry, LOG_LEVELS, } from "../src/protocol"; import { actor, json, REF } from "./fixtures"; const command: Command = { operation: "list_actors" }; const envelope = (reply: unknown) => json({ version: 5, request_id: "test", result: { Ok: reply } }); test("debugger replies validate breakpoint verification and explicit stop reasons", () => { const command: Command = { operation: "set_breakpoints", parameters: { actor_ref: REF, implementation_id: "impl", lines: [3, 1, 3] }, }; const breakpoints = [ { line: 1n, instruction_pointers: [7n, 18446744073709551615n] }, { line: 3n, instruction_pointers: [] }, ]; const reply = decodeResponse( envelope({ kind: "breakpoints_set", data: { breakpoints } }), "test", command, ); assert.deepEqual(reply, { kind: "breakpoints_set", data: { breakpoints: [ { line: 1, instruction_pointers: breakpoints[0]!.instruction_pointers }, { line: 3, instruction_pointers: [] }, ], }, }); for (const value of [ [], [...breakpoints].reverse(), [breakpoints[0], breakpoints[0]], [{ line: 1n, instruction_pointers: [2n, 2n] }, breakpoints[1]], [{ line: 1n, instruction_pointers: [-1n] }, breakpoints[1]], [{ line: 1n, instruction_pointers: [3n, 2n] }, breakpoints[1]], ]) assert.throws(() => decodeResponse( envelope({ kind: "breakpoints_set", data: { breakpoints: value } }), "test", command, ), ); const step: Command = { operation: "step_source", parameters: { actor_ref: REF, implementation_id: "impl", instruction_budget: 1000 }, }; for (const reason of [ "breakpoint", "step", "budget", "awaiting", "completed", "failed", "removed", ]) assert.deepEqual( decodeResponse(envelope({ kind: "debug_step_completed", data: { reason } }), "test", step), { kind: "debug_step_completed", data: { reason } }, ); for (const reason of [null, 1, "success", "paused"]) assert.throws(() => decodeResponse(envelope({ kind: "debug_step_completed", data: { reason } }), "test", step), ); }); test("execution inspections preserve u64 fields and validate stack order, bounds, identities and ranges", () => { const command: Command = { operation: "inspect_execution", parameters: { actor_ref: REF } }; const frame = { kind: "current", instruction_pointer: 18446744073709551615n, source_instruction_pointer: 18446744073709551615n, function: "nested", range: { start: { line: 1n, character: 2n }, end: { line: 1n, character: 5n } }, }; const execution = { vm_steps: 18446744073709551615n, awaiting: false, call_depth: 0n, frames: [frame], frames_truncated: false, }; const data = { actor_ref: REF, incarnation: 18446744073709551615n, implementation_id: "impl", source_id: "source", source: "exact source", behavior_path: "actors/test", execution, }; const decoded = decodeResponse(envelope({ kind: "execution_inspected", data }), "test", command); assert.equal(decoded.kind, "execution_inspected"); if (decoded.kind !== "execution_inspected") throw new Error("unexpected reply"); assert.equal(decoded.data.incarnation, data.incarnation); const debuggerState = { breakpoints: [{ line: 0n, instruction_pointers: [1n] }], stop_reason: "breakpoint", }; const withDebugger = decodeResponse( envelope({ kind: "execution_inspected", data: { ...data, debugger: debuggerState } }), "test", command, ); assert.equal(withDebugger.kind, "execution_inspected"); if (withDebugger.kind === "execution_inspected") assert.deepEqual(withDebugger.data.debugger, { ...debuggerState, breakpoints: [{ line: 0, instruction_pointers: [1n] }], }); assert.throws(() => decodeResponse( envelope({ kind: "execution_inspected", data: { ...data, debugger: { ...debuggerState, stop_reason: "invalid" } }, }), "test", command, ), ); assert.deepEqual(decoded.data.execution?.frames[0]?.range, { start: { line: 1, character: 2 }, end: { line: 1, character: 5 }, }); for (const change of [ { actor_ref: "ABCDEFGHIJKLMNOPQRSTU" }, { implementation_id: "" }, { source_id: "" }, { behavior_path: "../bad" }, { incarnation: -1n }, { execution: { ...execution, awaiting: "false" } }, { execution: { ...execution, frames: [] } }, { execution: { ...execution, frames_truncated: true } }, { execution: { ...execution, frames: [{ ...frame, kind: "caller" }] } }, { execution: { ...execution, frames: [{ ...frame, instruction_pointer: -1n }] } }, { execution: { ...execution, frames: [ { ...frame, range: { start: { line: 2n, character: 0n }, end: { line: 1n, character: 0n } }, }, ], }, }, { execution: { ...execution, frames: [ { ...frame, range: { start: { line: 9007199254740992n, character: 0n }, end: { line: 9007199254740992n, character: 0n }, }, }, ], }, }, ]) assert.throws(() => decodeResponse( envelope({ kind: "execution_inspected", data: { ...data, ...change } }), "test", command, ), ); assert.equal( decodeResponse( envelope({ kind: "execution_inspected", data: { ...data, execution: null } }), "test", command, ).kind, "execution_inspected", ); const unknown = { ...execution, frames: [{ ...frame, source_instruction_pointer: null, function: null, range: null }], }; assert.equal( decodeResponse( envelope({ kind: "execution_inspected", data: { ...data, execution: unknown } }), "test", command, ).kind, "execution_inspected", ); }); test("actor log lines use syslog priorities, exact sequence IDs and single-line messages", () => { const priorities = [135, 135, 134, 132, 131, 130]; for (const [index, level] of LOG_LEVELS.entries()) { assert.equal( formatLogEntry({ actor_ref: REF, level, sequence: 18446744073709551615n, message: "first\r\nsecond", }), `<${priorities[index]}>1 - - runa ${REF} 18446744073709551615 - [${level.toUpperCase()}] first\\r\\nsecond\n`, ); } }); test("actor behavior replies check identity, text and the requested operation", () => { const command: Command = { operation: "write_actor_behavior", parameters: { actor_ref: REF, expected_source_id: "original", source: "updated" }, }; const reply = { kind: "actor_behavior", data: { source: "updated", source_id: "temporary" } }; assert.deepEqual(decodeResponse(envelope(reply), "test", command), reply); for (const data of [ { source: "other", source_id: "temporary" }, { source: "updated", source_id: "" }, { source: 7, source_id: "temporary" }, ]) { assert.throws(() => decodeResponse(envelope({ kind: "actor_behavior", data }), "test", command), ); } assert.throws(() => decodeResponse(envelope(reply), "test", { operation: "list_sources" })); }); test("log pages preserve exact cursors, actor filters and strictly increasing sequences", () => { const command: Command = { operation: "logs", parameters: { actors: [REF], follow: false, after: 9223372036854775807n }, }; const entry = { sequence: 9223372036854775808n, actor_ref: REF, level: "info", message: "line" }; const data = { entries: [entry], next_sequence: entry.sequence, has_more: false }; assert.deepEqual(decodeResponse(envelope({ kind: "logs", data }), "test", command), { kind: "logs", data, }); for (const change of [ { entries: [entry, entry] }, { entries: [{ ...entry, actor_ref: "ABCDEFGHIJKLMNOPQRSTU" }] }, { entries: [{ ...entry, level: "unknown" }] }, { next_sequence: entry.sequence + 1n }, { entries: [], has_more: true, next_sequence: command.parameters.after }, { has_more: "false" }, ]) { assert.throws(() => decodeResponse(envelope({ kind: "logs", data: { ...data, ...change } }), "test", command), ); } }); test("notification envelopes preserve u64 revisions and reject malformed events", () => { const notification = (data: unknown) => json({ version: 5, 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("v5 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: 5, 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: 5, request_id: "test", result: { Err: { code: "not_found", message: "No actor" } }, }), "test", command, ), (error) => error instanceof ProtocolError && error.code === "not_found", ); }); test("structured diagnostics validate severity and exact editor coordinates on replies and errors", () => { const command: Command = { operation: "check_source", parameters: { source: "pub fn main() { missing }" }, }; const diagnostic = { severity: "error", message: "Missing variable", range: { start: { line: 0, character: 16 }, end: { line: 0, character: 23 } }, }; const reply = { kind: "source_checked", data: { diagnostics: [diagnostic] } }; assert.deepEqual(decodeResponse(envelope(reply), "test", command), reply); for (const change of [ { severity: "unknown" }, { message: "" }, { range: { start: { line: -1, character: 0 }, end: { line: 0, character: 0 } } }, { range: { start: { line: 0, character: 8 }, end: { line: 0, character: 2 } } }, { range: { start: { line: 9007199254740992n, character: 0 }, end: { line: 9007199254740992n, character: 1 }, }, }, ]) { assert.throws(() => decodeResponse( envelope({ kind: "source_checked", data: { diagnostics: [{ ...diagnostic, ...change }] } }), "test", command, ), ); } assert.deepEqual( decodeResponse( envelope({ kind: "source_checked", data: { diagnostics: [{ ...diagnostic, range: null, severity: "warning" }] }, }), "test", command, ), { kind: "source_checked", data: { diagnostics: [{ ...diagnostic, range: null, severity: "warning" }] }, }, ); assert.throws( () => decodeResponse( json({ version: 5, request_id: "test", result: { Err: { code: "runtime_failure", message: "Compile failed", diagnostics: [diagnostic] }, }, }), "test", command, ), (error) => error instanceof ProtocolError && error.diagnostics.length === 1 && error.diagnostics[0]?.range?.end.character === 23, ); }); test("mutation replies must match the requested operation and carry validated data", () => { const spawn: Command = { operation: "spawn", parameters: { path: "actors/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), ); const commands: Command[] = [ { operation: "send", parameters: { actor_ref: REF, message: { type: "null" } } }, ...(["stop", "kill", "restart", "pause", "resume", "reload"] as const).map((kind): Command => ({ operation: "system", parameters: { actor_ref: REF, message: { kind } }, })), { operation: "system", parameters: { actor_ref: REF, message: { kind: "become", path: "actors/counter" } }, }, ]; for (const command of commands) { assert.deepEqual(decodeResponse(envelope({ kind: "acknowledged" }), "test", command), { kind: "acknowledged", }); assert.throws(() => decodeResponse(envelope({ kind: "actors", data: [] }), "test", command)); } }); test("source paths are canonical extensionless relative paths", () => { for (const path of ["actors/counter", "a", "x/y_z/W-1"]) assert.ok(isSourcePath(path), path); for (const path of [ "", "/actors/counter", "../counter", "actors/../counter", "actors//counter", "source/actors/counter", "actors/counter.rn", "actors/my.counter", "actors\\counter", "actors/", "x".repeat(129), ]) assert.ok(!isSourcePath(path), path); assert.ok(isSourceRevision("abc123DEF")); for (const revision of ["", "1.2", "a-b", "x".repeat(65)]) assert.ok(!isSourceRevision(revision)); }); test("source listings and files validate paths, identities, and uniqueness", () => { const file = { path: "actors/counter", source_id: "source:1", source: "pub fn handle() {} // 雪\n", }; assert.deepEqual( decodeResponse(envelope({ kind: "sources", data: [file] }), "test", { operation: "list_sources", }), { kind: "sources", data: [file], }, ); for (const data of [ [{ ...file, path: "actors/counter.rn" }], [{ ...file, source_id: "" }], [{ ...file, source: 1 }], [file, file], ]) assert.throws(() => decodeResponse(envelope({ kind: "sources", data }), "test", { operation: "list_sources" }), ); const read: Command = { operation: "read_source", parameters: { path: file.path } }; assert.deepEqual(decodeResponse(envelope({ kind: "source", data: file }), "test", read), { kind: "source", data: file, }); assert.throws(() => decodeResponse( envelope({ kind: "source", data: { ...file, path: "actors/other" } }), "test", read, ), ); const write: Command = { operation: "write_source", parameters: { path: file.path, expected_source_id: null, source: file.source }, }; assert.equal( decodeResponse(envelope({ kind: "source", data: file }), "test", write).kind, "source", ); assert.throws(() => decodeResponse(envelope({ kind: "source", data: { ...file, source: "other" } }), "test", write), ); const rollback: Command = { operation: "rollback_source", parameters: { path: file.path, revision: "abc" }, }; assert.equal( decodeResponse(envelope({ kind: "source", data: file }), "test", rollback).kind, "source", ); assert.throws(() => decodeResponse(envelope({ kind: "sources", data: [] }), "test", read)); }); test("source commits and history carry readable revision strings", () => { const commit = { revision: "k3x9q2", source_id: "source:1" }; const path = { path: "actors/counter" }; assert.deepEqual( decodeResponse(envelope({ kind: "source_committed", data: commit }), "test", { operation: "commit_source", parameters: path, }), { kind: "source_committed", data: commit }, ); const history: Command = { operation: "source_history", parameters: path }; assert.deepEqual( decodeResponse(envelope({ kind: "source_history", data: [] }), "test", history), { kind: "source_history", data: [], }, ); for (const data of [ [{ ...commit, revision: "a.b" }], [{ ...commit, source_id: 1 }], [commit, commit], ]) assert.throws(() => decodeResponse(envelope({ kind: "source_history", data }), "test", history), ); assert.throws(() => decodeResponse(envelope({ kind: "source_committed", data: commit }), "test", history), ); }); test("revision source replies require text and the matching operation", () => { const command: Command = { operation: "revision_source", parameters: { path: "actors/counter", revision: "k3x9q2" }, }; const source = "pub fn handle(ctx, state, sender, message) {\n state // 雪\n}\n"; assert.deepEqual( decodeResponse(envelope({ kind: "behavior_source", data: { source } }), "test", command), { kind: "behavior_source", data: { source } }, ); for (const data of [{}, { source: null }, { source: 12 }, { source: [] }]) { assert.throws(() => decodeResponse(envelope({ kind: "behavior_source", data }), "test", command), ); } assert.throws(() => decodeResponse(envelope({ kind: "actors", data: [] }), "test", command)); assert.throws(() => decodeResponse(envelope({ kind: "behavior_source", data: { source } }), "test", { operation: "list_sources", }), ); }); 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/); });