import assert from "node:assert/strict"; import test from "node:test"; import { ImageClient } from "../src/client"; import { ProtocolError } from "../src/protocol"; import { parseValue } from "../src/values"; import { startImage, control } from "./server"; test( "actor breakpoints and source stepping preserve drafts, isolate actors and reject stale implementations", { timeout: 10000 }, async (t) => { const image = await startImage(); let changes = 0; let onChange: (() => void) | undefined; const client = await ImageClient.connect( image.address, (error) => assert.equal(error, undefined), () => { changes++; onChange?.(); }, ); t.after(async () => { await client.close(); await image.stop(); }); const source = "pub fn handle(ctx, state, sender, message) {\n ctx.send(ctx.this, 99);\n let next = nested(state);\n next\n}\nfn nested(value) {\n value + 1\n}"; const file = await client.writeSource("tests/debugger", null, source); const actor = await client.spawn(file.path, parseValue("3")); const other = await client.spawn(file.path, parseValue("10")); const loaded = await client.inspectExecution(actor); const bps = await client.setBreakpoints(actor, loaded.implementation_id, [2, 100, 2]); assert.deepEqual( bps.map((bp) => bp.line), [2, 100], ); assert.ok(bps[0]?.instruction_pointers.length); assert.deepEqual(bps[1]?.instruction_pointers, []); await assert.rejects( client.stepSource(actor, loaded.implementation_id), /paused actor with an active invocation/, ); await client.send(actor, parseValue("null")); await client.tick(1, 1000); const paused = await client.inspectActor(actor); const hit = await client.inspectExecution(actor); assert.equal(paused.status.status, "paused"); assert.equal(hit.debugger?.stop_reason, "breakpoint"); assert.equal(hit.execution?.frames[0]?.range?.start.line, 2); assert.deepEqual(paused.state, parseValue("3")); assert.equal(paused.queued.length, 0); assert.deepEqual(paused.reserved, parseValue("null")); await client.send(other, parseValue("null")); const otherBefore = await client.inspectActor(other); const count = changes; const changed = new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("step did not emit image_changed")), 2000); onChange = () => { clearTimeout(timer); onChange = undefined; resolve(); }; }); assert.equal(await client.stepSource(actor, hit.implementation_id), "step"); await changed; assert.ok(changes > count, "step changes broadcast image invalidation"); assert.deepEqual(await client.inspectActor(other), otherBefore); const nested = await client.inspectExecution(actor); assert.equal(nested.execution?.frames[0]?.function, "nested"); assert.equal(nested.debugger?.stop_reason, "step"); assert.deepEqual((await client.inspectActor(actor)).state, parseValue("3")); let completed = false; for (let i = 0; i < 20; i++) { if ((await client.stepSource(actor, hit.implementation_id)) === "completed") { completed = true; break; } } assert.ok(completed); const committed = await client.inspectActor(actor); assert.equal(committed.status.status, "paused"); assert.deepEqual(committed.state, parseValue("4")); assert.deepEqual(committed.queued, [parseValue("99")]); await client.setBreakpoints(actor, hit.implementation_id, []); await client.writeSource( file.path, file.source_id, "pub fn handle(ctx, state, sender, message) { 42 }", ); await client.system(actor, { kind: "reload" }); assert.equal((await client.inspectExecution(actor)).source, source); await client.system(actor, { kind: "resume" }); await client.tick(1, 100); assert.deepEqual((await client.inspectExecution(actor)).debugger, { breakpoints: [], stop_reason: null, }); await assert.rejects( client.setBreakpoints(actor, hit.implementation_id, [0]), (error) => error instanceof ProtocolError && error.code === "conflict", ); await assert.rejects( client.stepSource(actor, hit.implementation_id), (error) => error instanceof ProtocolError && error.code === "conflict", ); const active = await client.inspectExecution(actor); for (const budget of [0, 10001]) { const reply = await control(image.address, { operation: "step_source", parameters: { actor_ref: actor, implementation_id: active.implementation_id, instruction_budget: budget, }, }); assert.match(JSON.stringify(reply), /invalid_request/); } const invalid = await control(image.address, { operation: "set_breakpoints", parameters: { actor_ref: actor, implementation_id: active.implementation_id, lines: Array(129).fill(0), }, }); assert.match(JSON.stringify(invalid), /invalid_request/); }, );