2026-11-03 13:52:28 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import test from "node:test";
|
|
|
|
|
import { ImageClient } from "../src/client";
|
|
|
|
|
import { control, spawnCounter, startImage } from "./server";
|
|
|
|
|
|
2026-11-03 16:24:09 +00:00
|
|
|
test("extension disconnect and reconnect complete clean WebSocket handshakes", async (t) => {
|
|
|
|
|
const image = await startImage();
|
|
|
|
|
t.after(() => image.stop());
|
|
|
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
|
|
|
const client = await ImageClient.connect(image.address, (error) => {
|
|
|
|
|
assert.equal(error, undefined);
|
|
|
|
|
});
|
|
|
|
|
await client.listActors();
|
|
|
|
|
await client.close();
|
|
|
|
|
}
|
|
|
|
|
assert.doesNotMatch(image.diagnostics(), /protocol error|reset without closing handshake/i);
|
|
|
|
|
});
|
|
|
|
|
|
2026-11-03 14:36:21 +00:00
|
|
|
test(
|
|
|
|
|
"TypeScript inspection shares the real Rust image across clients and reconnects",
|
|
|
|
|
{ timeout: 10000 },
|
|
|
|
|
async (t) => {
|
|
|
|
|
const image = await startImage();
|
|
|
|
|
t.after(() => image.stop());
|
|
|
|
|
const actorRef = await spawnCounter(image.address);
|
|
|
|
|
const reference = { type: "actor_ref", data: actorRef };
|
|
|
|
|
const payload = {
|
|
|
|
|
type: "record",
|
|
|
|
|
data: {
|
|
|
|
|
nested: {
|
|
|
|
|
type: "list",
|
|
|
|
|
data: [reference, { type: "integer", data: -9223372036854775808n }],
|
|
|
|
|
},
|
2026-11-03 13:52:28 +00:00
|
|
|
},
|
2026-11-03 14:36:21 +00:00
|
|
|
};
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
await control(image.address, {
|
|
|
|
|
operation: "send",
|
|
|
|
|
parameters: { actor_ref: actorRef, message: payload },
|
|
|
|
|
}),
|
2026-11-03 16:01:24 +00:00
|
|
|
{ version: 4, request_id: "integration-control", result: { Ok: { kind: "acknowledged" } } },
|
2026-11-03 14:36:21 +00:00
|
|
|
);
|
2026-11-03 13:52:28 +00:00
|
|
|
|
2026-11-03 14:36:21 +00:00
|
|
|
const failures: Error[] = [];
|
|
|
|
|
const disconnected = (error?: Error) => {
|
|
|
|
|
if (error) failures.push(error);
|
|
|
|
|
};
|
|
|
|
|
const first = await ImageClient.connect(image.address, disconnected);
|
|
|
|
|
t.after(() => first.close());
|
|
|
|
|
const [snapshot] = await first.listActors();
|
|
|
|
|
assert.ok(snapshot);
|
|
|
|
|
assert.equal(snapshot.actor_ref, actorRef);
|
|
|
|
|
assert.deepEqual(snapshot.state, { type: "integer", data: 9223372036854775807n });
|
|
|
|
|
assert.deepEqual(snapshot.queued, [payload]);
|
|
|
|
|
assert.equal(snapshot.reserved, null);
|
|
|
|
|
first.close();
|
|
|
|
|
const revisions: bigint[] = [];
|
|
|
|
|
let onChanged = () => {};
|
|
|
|
|
const changed = new Promise<void>((resolve) => {
|
|
|
|
|
onChanged = resolve;
|
|
|
|
|
});
|
|
|
|
|
const second = await ImageClient.connect(image.address, disconnected, (revision) => {
|
|
|
|
|
revisions.push(revision);
|
|
|
|
|
onChanged();
|
|
|
|
|
});
|
|
|
|
|
t.after(() => second.close());
|
|
|
|
|
assert.deepEqual(await second.inspectActor(actorRef), snapshot);
|
|
|
|
|
await control(image.address, { operation: "pause_actor", parameters: { actor_ref: actorRef } });
|
|
|
|
|
await changed;
|
|
|
|
|
assert.equal(revisions.length, 1);
|
|
|
|
|
assert.ok(revisions[0] !== undefined && revisions[0] > 0n);
|
|
|
|
|
const paused = await second.inspectActor(actorRef);
|
|
|
|
|
assert.equal(paused.status.status, "paused");
|
|
|
|
|
// Reading from the extension must not drain the shared runtime event queue.
|
|
|
|
|
const events = await control(image.address, { operation: "get_events" });
|
|
|
|
|
assert.deepEqual(events, {
|
2026-11-03 16:01:24 +00:00
|
|
|
version: 4,
|
2026-11-03 14:36:21 +00:00
|
|
|
request_id: "integration-control",
|
|
|
|
|
result: {
|
|
|
|
|
Ok: {
|
|
|
|
|
kind: "events",
|
|
|
|
|
data: [
|
|
|
|
|
{ event: "spawned", data: { actor_ref: actorRef } },
|
|
|
|
|
{ event: "paused", data: { actor_ref: actorRef } },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(failures, []);
|
|
|
|
|
},
|
|
|
|
|
);
|