import assert from "node:assert/strict"; import { execFile, spawn } from "node:child_process"; import { once } from "node:events"; import { resolve } from "node:path"; import test from "node:test"; import { promisify } from "node:util"; import { ImageClient } from "../src/client"; import { startImage } from "./server"; test( "one-shot CLI commands share the image, report errors, and close cleanly", { timeout: 15000 }, async (t) => { const image = await startImage(); t.after(() => image.stop()); const executable = resolve( __dirname, "../../../target/debug", process.platform === "win32" ? "runa-cli.exe" : "runa-cli", ); const execute = promisify(execFile); const run = async (...args: string[]) => { const result = await execute(executable, ["--connect", image.address, ...args], { env: { ...process.env, RUNA_CONNECT: "invalid" }, timeout: 5000, }); assert.equal(result.stderr, ""); assert.doesNotMatch(result.stdout, /runa>|Connected to/); return result.stdout.trim(); }; assert.match(await run("sources"), /source\/actors\/counter\.rn/); await run("write", "actors/copy", resolve(__dirname, "../../../runa-std/actors/counter.rn")); const committed = await run("commit", "actors/copy"); const revision = committed.match(/^revision ([a-z0-9]+)$/)?.[1]; assert.ok(revision); assert.match(await run("history", "actors/copy"), new RegExp(revision)); assert.match(await run("rollback", "actors/copy", revision), /saved source\/actors\/copy\.rn/); const actor = await run("spawn", "actors/copy", "-5"); assert.match(actor, /^@[A-Za-z0-9_-]{21}$/); assert.match(await run("ps"), new RegExp(actor.slice(1))); assert.equal(await run("send", actor, '{"op":"add","value":7}'), "ok"); await run("tick", "100", "32"); assert.match(await run("inspect", actor), /Integer\(\s*2/); for (const command of ["pause", "resume", "reload"]) { await run(command, actor); } await run("become", actor, "actors/counter"); await run("tick", "100"); await run("stop", actor); await run("tick", "100"); assert.match(await run("inspect", actor), /Stopped/); await run("restart", actor); await run("kill", actor); assert.match(await run("events"), /Spawned/); for (const args of [ ["spawn", "actors/missing", "0"], ["spawn", "actors/counter", "invalid"], ["inspect", "not-an-actor"], ["actors"], ["cancel", actor], ]) { await assert.rejects(run(...args)); } assert.equal(await run("quit"), ""); assert.equal(await run("exit"), ""); assert.doesNotMatch(image.diagnostics(), /protocol error|reset without closing handshake/i); }, ); test( "the real REPL mutates the same WebSocket image and notifies the inspection client", { timeout: 10000 }, async (t) => { const image = await startImage(); let inspector: ImageClient | undefined; t.after(async () => { inspector?.close(); await image.stop(); }); const revisions: bigint[] = []; let onChanged = () => {}; const changed = new Promise((resolve) => { onChanged = resolve; }); inspector = await ImageClient.connect( image.address, (error) => assert.equal(error, undefined), (revision) => { revisions.push(revision); onChanged(); }, ); const executable = resolve( __dirname, "../../../target/debug", process.platform === "win32" ? "runa-cli.exe" : "runa-cli", ); const repl = spawn(executable, ["repl"], { env: { ...process.env, RUNA_CONNECT: image.address }, stdio: ["pipe", "pipe", "pipe"], }); t.after(() => { if (repl.exitCode === null && repl.signalCode === null) repl.kill(); }); let stdout = ""; let stderr = ""; repl.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString(); }); repl.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); const exited = once(repl, "exit"); repl.stdin.end( [ "let actor = spawn actors/counter 5", 'send actor {"op":"add","value":7}', "tick 100", "inspect actor", "quit", ].join("\n") + "\n", ); const [code] = await exited; assert.equal(code, 0, stderr); assert.equal(stderr, ""); assert.doesNotMatch(image.diagnostics(), /protocol error|reset without closing handshake/i); assert.doesNotMatch(stdout, /error:/i); await changed; assert.ok(revisions.length > 0); const [actor] = await inspector.listActors(); assert.ok(actor); assert.ok(stdout.includes(actor.actor_ref)); assert.deepEqual(actor.state, { type: "integer", data: 12n }); assert.deepEqual(actor.queued, []); assert.equal(actor.reserved, null); for (const input of ["ps\nquit\n", "exit\n", ""]) { const overridden = spawn(executable, ["--connect", image.address, "repl"], { env: { ...process.env, RUNA_CONNECT: "invalid" }, stdio: ["pipe", "pipe", "pipe"], }); t.after(() => { if (overridden.exitCode === null && overridden.signalCode === null) overridden.kill(); }); let diagnostics = ""; overridden.stderr.on("data", (chunk: Buffer) => { diagnostics += chunk.toString(); }); const overriddenExit = once(overridden, "exit"); overridden.stdin.end(input); const [overrideCode] = await overriddenExit; assert.equal(overrideCode, 0, diagnostics); assert.equal(diagnostics, ""); assert.doesNotMatch(image.diagnostics(), /protocol error|reset without closing handshake/i); } }, );