A handler is a live reference, held by the VatAdmin DO; workerd evicts an idle DO within seconds, and with it the handler, so a vat's page went 503 almost at once. createVat now keeps a vat's params when they hold only data and ingresses (an ingress by its key: the VatAdmin can make it again), and which vat was given which ingress. A request for a mount with no handler restarts that vat with its params; it rebuilds its root, serves again, and the request is answered. A vat whose params hold another peer's objects isn't restartable yet. The test crashes its own toybox (a crash ends every session to it), and the creator waits for the root's handoff before it disconnects, as scripts/hello.js now does too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
106 lines
4.1 KiB
JavaScript
106 lines
4.1 KiB
JavaScript
/** @file OCapN vats: a client on the admin socket creates a vat and passes it a remote object. */
|
|
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { after, before, test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { E, Far } from "@endo/far";
|
|
import { connectClient } from "../client.js";
|
|
import { freePort, here, startWorkerd } from "./workerd.js";
|
|
|
|
const vatSrc = (name) => readFile(join(here, "vats", name), "utf8");
|
|
|
|
let dir, port, server, client;
|
|
|
|
const startToybox = () =>
|
|
startWorkerd(
|
|
"config.capnp",
|
|
[
|
|
"-s", `http=127.0.0.1:${port}`, "-s", `admin=unix:${join(dir, "admin.sock")}`,
|
|
"-d", `state=${join(dir, "state")}`, "-e", `switchboard=unix:${join(dir, "no-switchboard.sock")}`,
|
|
],
|
|
{ port, host: "127.0.0.1" },
|
|
[join(dir, "admin.sock")],
|
|
);
|
|
|
|
before(async () => {
|
|
dir = await mkdtemp(join(tmpdir(), "toybox-ocapn-"));
|
|
await mkdir(join(dir, "state"));
|
|
port = await freePort();
|
|
server = await startToybox();
|
|
client = await connectClient(join(dir, "admin.sock"), "client-1", { verbose: !!process.env.OCAPN_LOG });
|
|
});
|
|
|
|
after(async () => {
|
|
client?.close();
|
|
await server?.stop();
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("createVat passes a remote object in; the vat's root comes back", async () => {
|
|
const carol = Far("carol", { greet: (name) => `hi ${name}, from carol` });
|
|
const { id, root } = await E(client.vatAdmin).createVat(await vatSrc("greeter.js"), { carol });
|
|
assert.match(id, /^[0-9a-f]{64}$/);
|
|
assert.equal(await E(root).hello(), "hello from the vat");
|
|
assert.equal(await E(root).greetVia("vat"), "hi vat, from carol");
|
|
});
|
|
|
|
test("a counter vat counts; two counter vats count apart", async () => {
|
|
const src = await vatSrc("counter.js");
|
|
const { root: a } = await E(client.vatAdmin).createVat(src);
|
|
const { root: b } = await E(client.vatAdmin).createVat(src);
|
|
assert.equal(await E(a).increment(), 1);
|
|
assert.equal(await E(a).increment(), 2);
|
|
assert.equal(await E(b).increment(), 1);
|
|
assert.equal(await E(a).read(), 2);
|
|
assert.equal(await E(b).read(), 1);
|
|
});
|
|
|
|
test("a vat serves the web through the ingress it is given", async () => {
|
|
const ingress = await E(client.vatAdmin).makeIngress({ vat: "hello" });
|
|
await E(client.vatAdmin).createVat(await vatSrc("hello.js"), { ingress });
|
|
const resp = await fetch(`http://127.0.0.1:${port}/greet?vat=hello`);
|
|
assert.equal(resp.status, 200);
|
|
assert.equal(resp.headers.get("content-type"), "text/html; charset=utf-8");
|
|
assert.match(await resp.text(), /<h1>Hello, world<\/h1><p>from a toybox vat, at \/greet<\/p>/);
|
|
});
|
|
|
|
test("an ingress nobody serves is a 503", async () => {
|
|
await E(client.vatAdmin).makeIngress({ vat: "idle" });
|
|
assert.equal((await fetch(`http://127.0.0.1:${port}/?vat=idle`)).status, 503);
|
|
});
|
|
|
|
test("after a crash, a request restarts the vat that serves it", async () => {
|
|
// its own toybox: a crash breaks every session to it, the shared client's too
|
|
const home = await mkdtemp(join(tmpdir(), "toybox-phoenix-"));
|
|
await mkdir(join(home, "state"));
|
|
const port2 = await freePort();
|
|
const start = () =>
|
|
startWorkerd(
|
|
"config.capnp",
|
|
[
|
|
"-s", `http=127.0.0.1:${port2}`, "-s", `admin=unix:${join(home, "admin.sock")}`,
|
|
"-d", `state=${join(home, "state")}`, "-e", `switchboard=unix:${join(home, "no-switchboard.sock")}`,
|
|
],
|
|
{ port: port2, host: "127.0.0.1" },
|
|
[join(home, "admin.sock")],
|
|
);
|
|
let toybox = await start();
|
|
try {
|
|
const creator = await connectClient(join(home, "admin.sock"), "creator");
|
|
const ingress = await E(creator.vatAdmin).makeIngress({ vat: "phoenix" });
|
|
const { root } = await E(creator.vatAdmin).createVat(await vatSrc("hello.js"), { ingress });
|
|
assert.equal((await fetch(`http://127.0.0.1:${port2}/?vat=phoenix`)).status, 200);
|
|
await root; // its handoff, settled before the client goes
|
|
creator.close();
|
|
await toybox.stop();
|
|
toybox = await start();
|
|
const resp = await fetch(`http://127.0.0.1:${port2}/again?vat=phoenix`);
|
|
assert.equal(resp.status, 200);
|
|
assert.match(await resp.text(), /Hello, world.*at \/again/);
|
|
} finally {
|
|
await toybox.stop();
|
|
await rm(home, { recursive: true, force: true });
|
|
}
|
|
});
|