2026-11-03 14:36:21 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { spawn } from "node:child_process";
|
|
|
|
|
import { once } from "node:events";
|
|
|
|
|
import { resolve } from "node:path";
|
|
|
|
|
import test from "node:test";
|
|
|
|
|
import { ImageClient } from "../src/client";
|
|
|
|
|
import { startImage } from "./server";
|
|
|
|
|
|
|
|
|
|
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<void>((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",
|
2026-11-03 14:49:35 +00:00
|
|
|
process.platform === "win32" ? "runa-cli.exe" : "runa-cli",
|
2026-11-03 14:36:21 +00:00
|
|
|
);
|
2026-11-03 14:49:35 +00:00
|
|
|
const repl = spawn(executable, [], {
|
|
|
|
|
env: { ...process.env, RUNA_CONNECT: image.address },
|
2026-11-03 14:36:21 +00:00
|
|
|
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(
|
|
|
|
|
[
|
2026-11-03 16:01:24 +00:00
|
|
|
"let actor = spawn actors/counter 5",
|
2026-11-03 14:36:21 +00:00
|
|
|
'send actor {"op":"add","value":7}',
|
|
|
|
|
"tick 100",
|
|
|
|
|
"inspect actor",
|
|
|
|
|
"quit",
|
|
|
|
|
].join("\n") + "\n",
|
|
|
|
|
);
|
|
|
|
|
const [code] = await exited;
|
|
|
|
|
assert.equal(code, 0, stderr);
|
2026-11-03 16:05:29 +00:00
|
|
|
assert.equal(stderr, "");
|
|
|
|
|
assert.doesNotMatch(image.diagnostics(), /protocol error|reset without closing handshake/i);
|
2026-11-03 14:36:21 +00:00
|
|
|
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);
|
2026-11-03 16:05:29 +00:00
|
|
|
for (const input of ["actors\nquit\n", "exit\n", ""]) {
|
|
|
|
|
const overridden = spawn(executable, ["--connect", image.address], {
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-11-03 14:36:21 +00:00
|
|
|
},
|
|
|
|
|
);
|