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, } 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("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("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/); });