chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -0,0 +1,328 @@
import { describe, expect, it } from "vitest";
import { renderDeclarations, renderToolSample, renderToolSignature, schemaToType } from "../src/index.ts";
const execute = () => undefined;
describe("schemaToType", () => {
it("renders primitives, literals, and unions", () => {
expect(schemaToType({ type: "string" })).toBe("string");
expect(schemaToType({ type: "integer" })).toBe("number");
expect(schemaToType({ type: ["string", "null"] })).toBe("string | null");
expect(schemaToType({ const: "a" })).toBe('"a"');
expect(schemaToType({ enum: ["a", 1, null] })).toBe('"a" | 1 | null');
expect(schemaToType({ anyOf: [{ type: "string" }, { type: "number" }] })).toBe("string | number");
expect(schemaToType({ anyOf: [{ type: "string" }, {}] })).toBe("unknown");
expect(schemaToType({ allOf: [{ anyOf: [{ type: "string" }, { type: "number" }] }, { const: 1 }] })).toBe(
"(string | number) & 1",
);
expect(schemaToType({ $ref: "#/defs/x" })).toBe("unknown");
expect(schemaToType(true)).toBe("unknown");
expect(schemaToType(false)).toBe("never");
});
it("renders objects on one line with sorted properties", () => {
expect(
schemaToType({
type: "object",
properties: { city: { type: "string" }, "max-lines": { type: "number" } },
required: ["city"],
additionalProperties: false,
}),
).toBe('{ city: string; "max-lines"?: number; }');
expect(schemaToType({ type: "object", additionalProperties: { type: "number" } })).toBe(
"{ [key: string]: number; }",
);
expect(schemaToType({ type: "object" })).toBe("{ [key: string]: unknown; }");
expect(schemaToType({ type: "object", properties: {}, additionalProperties: false })).toBe("{}");
});
it("puts property descriptions on comment lines", () => {
expect(
schemaToType({
type: "object",
properties: {
weather: {
type: "array",
description: "look up weather for a given list of locations",
items: { type: "object", properties: { location: { type: "string" } }, required: ["location"] },
},
},
required: ["weather"],
}),
).toBe("{\n // look up weather for a given list of locations\n weather: Array<{ location: string; }>;\n}");
expect(
schemaToType({
type: "object",
properties: {
outer: {
type: "object",
description: "Outer",
properties: { inner: { type: "string", description: "Inner" } },
},
},
}),
).toBe("{\n // Outer\n outer?: {\n // Inner\n inner?: string;\n };\n}");
});
// https://github.com/earendil-works/pi/issues/10707
it("puts constraints the type cannot express on property comment lines", () => {
expect(
schemaToType({
type: "object",
properties: {
timeoutMs: {
type: "integer",
minimum: 0,
maximum: 30000,
default: 0,
description: "Wait in milliseconds.",
},
id: { type: "string", pattern: "^[a-z]+$", maxLength: 8 },
tags: { type: "array", items: { type: "string" }, maxItems: 5, uniqueItems: true, deprecated: true },
name: { type: "string" },
},
required: ["id"],
}),
).toBe(
[
"{",
" // @maxLength 8 @pattern ^[a-z]+$",
" id: string;",
" name?: string;",
" // @maxItems 5 @uniqueItems @deprecated",
" tags?: Array<string>;",
" // Wait in milliseconds.",
" // @integer @minimum 0 @maximum 30000 @default 0",
" timeoutMs?: number;",
"}",
].join("\n"),
);
});
it("reads constraints of optional fields from the non-null branch", () => {
// Pydantic's `limit: int | None = Field(None, ge=1, le=100)`.
expect(
schemaToType({
type: "object",
properties: {
limit: {
anyOf: [{ type: "integer", minimum: 1, maximum: 100 }, { type: "null" }],
default: null,
title: "Limit",
},
},
}),
).toBe("{\n // @integer @minimum 1 @maximum 100\n limit?: number | null;\n}");
// Constraints of the property and the branch both apply.
expect(
schemaToType({
type: "object",
properties: {
n: {
anyOf: [{ type: "integer", minimum: 10, maximum: 50, format: "int32" }, { type: "null" }],
minimum: 0,
maximum: 20,
format: "port",
},
},
}),
).toBe("{\n // @integer @minimum 10 @maximum 20 @format port @format int32\n n?: number | null;\n}");
});
it("reads draft-04 boolean exclusive bounds", () => {
expect(
schemaToType({
type: "object",
properties: {
n: { type: "integer", minimum: 0, exclusiveMinimum: true, maximum: 10, exclusiveMaximum: false },
m: { anyOf: [{ type: "number", maximum: 5, exclusiveMaximum: true }, { type: "null" }], maximum: 8 },
},
}),
).toBe(
[
"{",
" // @maximum 8 @exclusiveMaximum 5",
" m?: number | null;",
" // @integer @exclusiveMinimum 0 @maximum 10",
" n?: number;",
"}",
].join("\n"),
);
});
it("leaves out long tags and escapes line terminators", () => {
expect(
schemaToType({
type: "object",
properties: { config: { type: "object", default: { text: "x".repeat(100) } } },
}),
).toBe("{ config?: { [key: string]: unknown; }; }");
expect(
schemaToType({
type: "object",
properties: {
sep: { type: "string", pattern: "^[\r\n\u2028\u2029]$", default: "\u2028", description: "a\u2028b" },
},
}),
).toBe(
[
"{",
" // a",
" // b",
' // @pattern ^[\\u000d\\u000a\\u2028\\u2029]$ @default "\\u2028"',
" sep?: string;",
"}",
].join("\n"),
);
});
it("drops constraint tags before rendering a type over the budget as unknown", () => {
const schema = { type: "object", properties: { n: { type: "integer", minimum: 1 } } };
expect(schemaToType(schema)).toBe("{\n // @integer @minimum 1\n n?: number;\n}");
expect(schemaToType(schema, { maxChars: 20 })).toBe("{ n?: number; }");
expect(schemaToType(schema, { maxChars: 10 })).toBe("unknown");
});
it("resolves local references and stops at recursive ones", () => {
const schema = {
type: "object",
properties: {
item: { $ref: "#/$defs/Item" },
legacy: { $ref: "#/definitions/Legacy" },
remote: { $ref: "https://example.com/schema.json" },
},
required: ["item"],
$defs: {
Item: {
type: "object",
properties: { id: { type: "string" }, parent: { $ref: "#/$defs/Item" } },
required: ["id"],
},
},
definitions: { Legacy: { enum: ["a", "b"] } },
};
expect(schemaToType(schema)).toBe(
'{ item: { id: string; parent?: unknown; }; legacy?: "a" | "b"; remote?: unknown; }',
);
});
it("renders arrays and tuples", () => {
expect(schemaToType({ type: "array", items: { type: "string" } })).toBe("Array<string>");
expect(schemaToType({ type: "array", prefixItems: [{ type: "string" }, { type: "number" }] })).toBe(
"[string, number]",
);
expect(schemaToType({ type: "array" })).toBe("unknown[]");
});
it("renders types over the budget as unknown", () => {
const properties = Object.fromEntries(Array.from({ length: 50 }, (_, i) => [`field${i}`, { type: "string" }]));
const schema = { type: "object", properties };
expect(schemaToType(schema, { maxChars: 100 })).toBe("unknown");
expect(schemaToType(schema)).toContain("field49?: string;");
});
});
describe("tool declarations", () => {
it("renders signatures with normalized identifiers", () => {
expect(
renderToolSignature({
name: "hidden-dynamic-tool",
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
}),
).toBe("hidden_dynamic_tool(args: { city: string; }): Promise<{ ok: boolean; }>;");
expect(renderToolSignature({ name: "free" })).toBe("free(args: unknown): Promise<unknown>;");
});
it("renders output schemas shaped like an MCP CallToolResult as plain types", () => {
const outputSchema = {
type: "object",
properties: {
content: { type: "array", items: { type: "object" } },
isError: { type: "boolean" },
_meta: { type: "object" },
},
required: ["content"],
};
expect(
renderToolSignature({ name: "plain", inputSchema: { type: "object", properties: {} }, outputSchema }),
).toBe(
"plain(args: { [key: string]: unknown; }): Promise<{ _meta?: { [key: string]: unknown; }; content: Array<{ [key: string]: unknown; }>; isError?: boolean; }>;",
);
});
it("renders the per-tool sample", () => {
expect(renderToolSample({ name: "foo", description: "bar", inputSchema: { type: "string" } })).toBe(
"bar\n\ncodemode tool declaration:\n```ts\ndeclare const tools: { foo(args: string): Promise<unknown>; };\n```",
);
});
});
describe("renderDeclarations", () => {
it("renders tools and globals", () => {
const text = renderDeclarations({
tools: [
{
name: "read",
description: "Read a file.\nSecond line.",
inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
outputSchema: { type: "string" },
execute,
},
{ name: "remote-api", execute },
],
globals: [{ name: "attach", description: "Attach it.", inputSchema: { type: "string" }, execute }],
});
expect(text).toBe(
[
"declare const tools: {",
" /**",
" * Read a file.",
" * Second line.",
" */",
" read(args: { path: string; }): Promise<string>;",
" remote_api(args: unknown): Promise<unknown>;",
"};",
"",
"/** Attach it. */",
"declare function attach(args: string): Promise<unknown>;",
].join("\n"),
);
});
it("renders namespaced globals and explicit signatures", () => {
const text = renderDeclarations({
globals: [
{
name: "models.list",
description: "List models.",
signature: "(type: string): Promise<string[]>",
execute,
},
{ name: "models.get", inputSchema: { type: "string" }, execute },
{ name: "plain", signature: "(): void", execute },
],
});
expect(text).toBe(
[
"declare function plain(): void;",
"",
"declare const models: {",
" /** List models. */",
" list(type: string): Promise<string[]>;",
" get(args: string): Promise<unknown>;",
"};",
].join("\n"),
);
});
it("escapes comment terminators in descriptions", () => {
const text = renderDeclarations({ tools: [{ name: "x", description: "a */ b", execute }] });
expect(text).toContain("/** a *\\/ b */");
});
});

View File

@@ -0,0 +1,410 @@
import { afterEach, describe, expect, it } from "vitest";
import {
type CodemodeHostCall,
type CodemodeOutcome,
type CodemodeRemoteExchange,
type CodemodeRun,
type CodemodeRunOptions,
CodemodeSandbox,
type CodemodeSandboxBase,
type CodemodeSandboxBaseOptions,
loadQuickJSWasm,
RemoteCodemodeSandbox,
serveCodemodeRemote,
} from "../src/index.ts";
/**
* `start()` runs a script deterministically with the caller answering its calls, so a durable host can replay it
* after a restart. Every test runs on the worker sandbox (Node) and the remote sandbox (Cloudflare), whose transport
* clones every message as Workers RPC would.
*/
const BUDGET = 50_000;
/** `delayFirst` holds the reply to the first result back, so later results reach the VM before it. */
function remote(options: CodemodeSandboxBaseOptions, delayFirst = false): RemoteCodemodeSandbox {
return new RemoteCodemodeSandbox({
...options,
remote: () => ({
execute: (request, exchange) => {
const wrapped: CodemodeRemoteExchange = async (messages) => {
const reply = structuredClone(await exchange(structuredClone(messages)));
if (delayFirst && reply?.type === "result" && reply.seq === 1) {
await new Promise((resolve) => setTimeout(resolve, 30));
}
return reply;
};
return serveCodemodeRemote(structuredClone(request), wrapped, {
wasm: loadQuickJSWasm(),
maxInterruptBudget: BUDGET,
});
},
}),
});
}
const BACKENDS: Record<string, (options?: CodemodeSandboxBaseOptions) => CodemodeSandboxBase> = {
worker: (options = {}) => new CodemodeSandbox(options),
remote: (options = {}) => remote(options),
};
const STARTED_AT = Date.UTC(2026, 0, 15, 10, 0, 0);
type Recorded = {
run: CodemodeRun;
calls: CodemodeHostCall[];
idles: [seq: number, issued: number][];
events: string[];
fingerprint: () => string | undefined;
};
/**
* Start `code` with tools `echo` and `wait`. Every call but `wait` is answered at once, by default with its argument,
* as result number 1, 2, ... with the clock at `STARTED_AT + 1000 * seq`; `wait` is left for the test to answer.
*/
function start(
sandbox: CodemodeSandboxBase,
code: string,
options: Partial<CodemodeRunOptions> & { answer?: (call: CodemodeHostCall) => CodemodeOutcome } = {},
): Recorded {
const calls: CodemodeHostCall[] = [];
const idles: [number, number][] = [];
const events: string[] = [];
let fingerprint: string | undefined;
let seq = 0;
const answer = options.answer ?? ((call: CodemodeHostCall) => ({ ok: true, value: call.args }));
const run: CodemodeRun = sandbox.start(code, {
startedAt: STARTED_AT,
seed: 1,
tools: [{ name: "echo" }, { name: "wait" }],
timeoutMs: 20_000,
...options,
onStarted: (value) => {
fingerprint = value;
events.push("started");
},
onCall: (call) => {
calls.push(call);
events.push(`call ${call.id}`);
if (call.name === "wait") return;
queueMicrotask(() => {
seq++;
run.deliver(seq, STARTED_AT + 1000 * seq, call.id, answer(call));
});
},
onIdle: (seq, issued) => {
idles.push([seq, issued]);
events.push(`idle ${seq}`);
},
});
return { run, calls, idles, events, fingerprint: () => fingerprint };
}
const savedTimezone = process.env.TZ;
afterEach(() => {
if (savedTimezone === undefined) delete process.env.TZ;
else process.env.TZ = savedTimezone;
});
for (const [backend, create] of Object.entries(BACKENDS)) {
describe(`deterministic runs (${backend})`, () => {
it("makes the same calls and output for the same start, seed, and results", async () => {
const code = `
const values = [Date.now(), performance.now(), Math.random(), Math.random(), new Date().toISOString()];
const first = await tools.echo(values);
text(JSON.stringify([Date.now(), performance.now(), Math.random()]));
return await tools.echo(first.concat(Math.random()));
`;
const sandbox = create();
const one = start(sandbox, code);
const two = start(sandbox, code);
const [a, b] = await Promise.all([one.run.result, two.run.result]);
// Call durations are wall-clock time; everything else is the same.
const timeless = (result: typeof a) => ({
...result,
calls: result.calls.map(({ name, status }) => ({ name, status })),
});
expect(timeless(a)).toEqual(timeless(b));
expect(one.calls).toEqual(two.calls);
// The clock: startedAt, then the time of the result being processed; performance.now() counts from startedAt.
const [now, elapsed, random] = one.calls[0]!.args as number[];
expect([now, elapsed]).toEqual([STARTED_AT, 0]);
expect((one.calls[0]!.args as unknown[])[4]).toBe("2026-01-15T10:00:00.000Z");
const printed = JSON.parse(a.ok ? (a.output[0] as { text: string }).text : "[]") as number[];
expect(printed.slice(0, 2)).toEqual([STARTED_AT + 1000, 1000]);
// Pinned: a change here changes what replayed scripts see, so it must change the fingerprint too.
expect(random).toBe(PINNED_FIRST_RANDOM);
// The seed decides the random values, not the start time.
const later = start(sandbox, code, { startedAt: STARTED_AT + 5000 });
const reseeded = start(sandbox, code, { seed: 2 });
await Promise.all([later.run.result, reseeded.run.result]);
expect((later.calls[0]!.args as number[])[2]).toBe(random);
expect((reseeded.calls[0]!.args as number[])[2]).not.toBe(random);
// Seed 0 has its own values: QuickJS turns a zero state into 1, which must not make it seed 1's.
const zero = start(sandbox, "return [Math.random(), Math.random()]", { seed: 0 });
const zeroResult = await zero.run.result;
expect(zeroResult).toMatchObject({ ok: true, value: PINNED_SEED_ZERO });
expect((zeroResult.ok ? zeroResult.value : []) as number[]).not.toContain(random);
});
it("runs in UTC whatever the host's timezone, and without WeakRef or FinalizationRegistry", async () => {
process.env.TZ = "Asia/Kolkata";
const code =
"return [new Date(0).getHours(), new Date(0).getTimezoneOffset(), typeof WeakRef, typeof FinalizationRegistry]";
const sandbox = create();
const { run } = start(sandbox, code);
expect(await run.result).toMatchObject({ ok: true, value: [0, 0, "undefined", "undefined"] });
// execute() is unchanged: the host's timezone and every intrinsic.
expect(await sandbox.execute(code)).toMatchObject({ ok: true, value: [5, -330, "function", "function"] });
});
it("applies results in seq order, whatever order they are delivered in", async () => {
const sandbox = create();
const recorded = start(
sandbox,
`const order = [];
await Promise.all([tools.wait("a").then(() => order.push("a")), tools.wait("b").then(() => order.push("b"))]);
return order;`,
);
await waitFor(() => recorded.calls.length === 2 && recorded.idles.length === 1);
const [a, b] = recorded.calls;
// b is result 1 and a result 2, but a arrives first.
recorded.run.deliver(2, STARTED_AT + 2000, a!.id, { ok: true, value: null });
recorded.run.deliver(1, STARTED_AT + 1000, b!.id, { ok: true, value: null });
expect(await recorded.run.result).toMatchObject({ ok: true, value: ["b", "a"] });
// No idle after the last result: the script finished in that drain, and the host stopped the VM.
expect(recorded.idles).toEqual([
[0, 2],
[1, 2],
]);
});
it("reports the fingerprint before any call and the calls issued after each result", async () => {
const sandbox = create({ memoryLimitBytes: 64 * 1024 * 1024 });
const recorded = start(sandbox, "await tools.echo(1); await Promise.all([tools.echo(2), tools.echo(3)]);");
expect(await recorded.run.result).toMatchObject({ ok: true });
expect(recorded.events.slice(0, 3)).toEqual(["started", "call 1", "idle 0"]);
expect(recorded.idles).toEqual([
[0, 1],
[1, 3],
[2, 3],
]);
expect(recorded.calls.map((call) => [call.id, call.target, call.name, call.args])).toEqual([
[1, "tool", "echo", 1],
[2, "tool", "echo", 2],
[3, "tool", "echo", 3],
]);
const fingerprint = recorded.fingerprint()!;
expect(fingerprint).toMatch(
/^quickjs-wasi \S+, quickjs \S+, prelude [0-9a-f]{8}, deterministic \d+, stack \d+, heap 67108864, budget \S+$/,
);
if (backend === "remote") expect(fingerprint).toContain(`budget ${BUDGET}`);
else expect(fingerprint).toContain("budget none");
const same = start(sandbox, "return 1");
const other = start(create({ memoryLimitBytes: 32 * 1024 * 1024 }), "return 1");
await Promise.all([same.run.result, other.run.result]);
expect(same.fingerprint()).toBe(fingerprint);
expect(other.fingerprint()).not.toBe(fingerprint);
});
it("numbers calls to globals with tools, and answers errors as rejections", async () => {
const sandbox = create();
const recorded = start(
sandbox,
`const found = await search("x", 2);
try { await tools.echo("fail"); } catch (error) { text(error.message); }
return found;`,
{
globals: [{ name: "search", spread: true }],
answer: (call) =>
call.args === "fail"
? { ok: false, message: "no" }
: { ok: true, value: call.name === "search" ? ["found"] : call.args },
},
);
const result = await recorded.run.result;
expect(recorded.calls[0]).toEqual({ id: 1, target: "global", name: "search", args: ["x", 2] });
expect(result).toMatchObject({ ok: true, value: ["found"], output: [{ type: "text", text: "no" }] });
expect(recorded.calls[1]).toMatchObject({ id: 2, target: "tool", name: "echo" });
// Calls to globals count as issued calls too.
expect(recorded.idles.slice(0, 2)).toEqual([
[0, 1],
[1, 2],
]);
expect(result.calls).toMatchObject([{ name: "echo", status: "error" }]);
});
it("keeps the clock monotonic and rejects a clock value that is not finite", async () => {
const sandbox = create();
const recorded = start(
sandbox,
"const before = Date.now(); await tools.wait(1); return [before, Date.now(), performance.now()]",
);
await waitFor(() => recorded.calls.length === 1);
expect(() => recorded.run.deliver(1, Number.NaN, 1, { ok: true, value: null })).toThrow("not a finite number");
// A resumed host whose clock is behind the run's start.
recorded.run.deliver(1, STARTED_AT - 2000, 1, { ok: true, value: null });
expect(await recorded.run.result).toMatchObject({ ok: true, value: [STARTED_AT, STARTED_AT, 0] });
});
it("reports the result the VM was processing when the script ended", async () => {
const sandbox = create();
const returned = start(sandbox, "await tools.echo(1); return await tools.echo(2)");
const thrown = start(
sandbox,
"await tools.echo(1); await Promise.all([tools.echo(2), tools.echo(3)]); throw new Error('x')",
);
const first = start(sandbox, "return 1");
expect(await returned.run.result).toMatchObject({ ok: true, value: 2, during: { seq: 2, issued: 2 } });
expect(await thrown.run.result).toMatchObject({ ok: false, during: { seq: 3, issued: 3 } });
expect(await first.run.result).toMatchObject({ ok: true, during: { seq: 0, issued: 0 } });
expect(await sandbox.execute("return 1")).not.toHaveProperty("during");
});
it("leaves a call waiting when its answer cannot be serialized", async () => {
const sandbox = create();
const recorded = start(sandbox, "return await tools.wait(1)");
await waitFor(() => recorded.calls.length === 1);
expect(() => recorded.run.deliver(1, STARTED_AT, 1, { ok: true, value: 1n })).toThrow();
expect(() =>
recorded.run.deliver(1, STARTED_AT, 1, { ok: false, message: "x", properties: { big: 1n } }),
).toThrow();
recorded.run.deliver(1, STARTED_AT, 1, { ok: true, value: "fine" });
expect(await recorded.run.result).toMatchObject({ ok: true, value: "fine" });
});
it("rejects with an Error carrying the answer's properties", async () => {
const sandbox = create();
const recorded = start(
sandbox,
"try { await tools.echo(1); } catch (e) { return [e instanceof Error, e.message, e.name, e.structuredOutput]; }",
{
answer: () => ({
ok: false,
message: "denied",
properties: { structuredOutput: { code: 7 }, name: "Ignored", message: "ignored" },
}),
},
);
expect(await recorded.run.result).toMatchObject({ ok: true, value: [true, "denied", "Error", { code: 7 }] });
});
it("rejects rejection properties that are not an object, and tools sharing a script name", async () => {
const sandbox = create();
const recorded = start(sandbox, "return await tools.wait(1)");
await waitFor(() => recorded.calls.length === 1);
for (const properties of [null, [1]] as unknown as Record<string, unknown>[]) {
expect(() => recorded.run.deliver(1, STARTED_AT, 1, { ok: false, message: "x", properties })).toThrow(
"not an object",
);
}
recorded.run.deliver(1, STARTED_AT, 1, { ok: true, value: 1 });
expect(await recorded.run.result).toMatchObject({ ok: true, value: 1 });
expect(() =>
sandbox.start("", {
startedAt: STARTED_AT,
seed: 1,
tools: [{ name: "a-b" }, { name: "a_b" }],
onCall: () => {},
}),
).toThrow("script names must be unique");
});
it("uses the script names the run passes", async () => {
const sandbox = create();
const recorded = start(
sandbox,
"return [typeof tools.renamed, Object.keys(tools), ALL_TOOLS.map((tool) => tool.name)]",
{
tools: [{ name: "my-tool", jsName: "renamed" }],
},
);
expect(await recorded.run.result).toMatchObject({
ok: true,
value: ["function", ["renamed", "my-tool"], ["renamed"]],
});
});
it("ends with an error the script cannot catch when the caller aborts", async () => {
const sandbox = create();
const controller = new AbortController();
const recorded = start(
sandbox,
`try { await tools.wait(1); } catch { text("caught"); }
text("after");`,
{ signal: controller.signal },
);
await waitFor(() => recorded.calls.length === 1);
controller.abort(new Error("diverged"));
const result = await recorded.run.result;
expect(result).toMatchObject({ ok: false, error: { kind: "aborted", message: "diverged" }, output: [] });
expect(() => recorded.run.deliver(1, STARTED_AT, 1, { ok: true, value: null })).not.toThrow();
});
it("is finished once it has its outcome", async () => {
const sandbox = create();
const recorded = start(sandbox, "await tools.wait(1); return 2");
await waitFor(() => recorded.calls.length === 1);
expect(recorded.run.finished).toBe(false);
recorded.run.deliver(1, STARTED_AT, 1, { ok: true, value: null });
await recorded.run.result;
expect(recorded.run.finished).toBe(true);
});
it("rejects a call that is not waiting and a result number used twice", async () => {
const sandbox = create();
const recorded = start(sandbox, "await Promise.all([tools.wait(1), tools.wait(2)])");
await waitFor(() => recorded.calls.length === 2);
const { run } = recorded;
expect(() => run.deliver(1, STARTED_AT, 99, { ok: true, value: null })).toThrow("Call 99 is not waiting");
run.deliver(1, STARTED_AT, 1, { ok: true, value: null });
expect(() => run.deliver(1, STARTED_AT, 2, { ok: true, value: null })).toThrow("used once");
expect(() => run.deliver(0, STARTED_AT, 2, { ok: true, value: null })).toThrow("used once");
run.deliver(2, STARTED_AT, 2, { ok: true, value: null });
expect(await run.result).toMatchObject({ ok: true });
});
});
}
describe("deterministic runs (remote transport)", () => {
it("applies results in seq order when the transport reorders replies sent in order", async () => {
const sandbox = remote({}, true);
const recorded = start(
sandbox,
`const order = [];
await Promise.all([tools.wait("a").then(() => order.push("a")), tools.wait("b").then(() => order.push("b"))]);
return order;`,
);
await waitFor(() => recorded.calls.length === 2);
const [a, b] = recorded.calls;
recorded.run.deliver(1, STARTED_AT + 1000, a!.id, { ok: true, value: null });
recorded.run.deliver(2, STARTED_AT + 2000, b!.id, { ok: true, value: null });
expect(await recorded.run.result).toMatchObject({ ok: true, value: ["a", "b"] });
});
});
describe("execute() is not deterministic", () => {
it("reads the real clock and real randomness", async () => {
const sandbox = new CodemodeSandbox();
const before = Date.now();
const first = await sandbox.execute("return [Date.now(), Math.random()]");
const second = await sandbox.execute("return [Date.now(), Math.random()]");
const [now, random] = (first.ok ? first.value : []) as number[];
expect(now).toBeGreaterThanOrEqual(before);
expect(now).toBeLessThanOrEqual(Date.now());
expect((second.ok ? second.value : []) as number[]).not.toContain(random);
});
});
async function waitFor(check: () => boolean): Promise<void> {
const deadline = Date.now() + 5000;
while (!check()) {
if (Date.now() > deadline) throw new Error("Condition was not reached");
await new Promise((resolve) => setTimeout(resolve, 5));
}
}
/** The first two `Math.random()` values with seed 0. */
const PINNED_SEED_ZERO = [0.6711372530266764, 0.7258461452833667];
/** The first `Math.random()` with seed 1. */
const PINNED_FIRST_RANDOM = 0.34227450605335275;

View File

@@ -0,0 +1,8 @@
/**
* Stands in for the real worker in host tests: posts the JSON message (or array of messages) passed
* as the script source, so tests can send the host payloads the real prelude never produces.
*/
import { parentPort, workerData } from "node:worker_threads";
const messages: unknown = JSON.parse((workerData as { code: string }).code);
for (const message of Array.isArray(messages) ? messages : [messages]) parentPort?.postMessage(message);

View File

@@ -0,0 +1,51 @@
import { CodemodeSandbox } from "../../src/runtime/host.ts";
const how = process.argv[2];
if (how !== "abort" && how !== "timeout" && how !== "close") throw new Error(`Unknown stop mode: ${how}`);
const started = performance.now();
process.on("exit", () => {
console.log(JSON.stringify({ type: "exit", elapsedMs: performance.now() - started }));
});
const controller = new AbortController();
let timer: ReturnType<typeof setTimeout> | undefined;
let workerExited = false;
const sandbox = new CodemodeSandbox({
memoryLimitBytes: 256 * 1024 * 1024,
onWorkerStarted: (exited) => {
void exited.then(() => {
workerExited = true;
});
},
tools: [
{
name: "ready",
execute: () => {
if (how !== "timeout") {
timer = setTimeout(() => {
if (how === "abort") controller.abort();
else void sandbox.close();
}, 100);
}
},
},
],
});
try {
const result = await sandbox.execute(
"await tools.ready(); return 'a'.repeat(300000).indexOf('a'.repeat(10000) + 'b')",
{ signal: controller.signal, timeoutMs: how === "timeout" ? 300 : Number.POSITIVE_INFINITY },
);
if (result.ok) throw new Error("The script completed instead of being cancelled");
await sandbox.close();
console.log(
JSON.stringify({
type: "result",
kind: result.error.kind,
elapsedMs: performance.now() - started,
workerExited,
}),
);
} finally {
clearTimeout(timer);
await sandbox.close();
}

View File

@@ -0,0 +1,159 @@
import { describe, expect, it } from "vitest";
import {
type CodemodeRemote,
type CodemodeRemoteExchange,
type CodemodeRemoteRequest,
type CodemodeServeOptions,
InlineCodemodeSandbox,
loadQuickJSWasm,
RemoteCodemodeSandbox,
serveCodemodeRemote,
} from "../src/index.ts";
const wasm = loadQuickJSWasm();
/** An in-process remote that records what crosses the boundary, cloned as Workers RPC would. */
function recordingRemote(options: Partial<CodemodeServeOptions> = {}) {
const exchanges: number[] = [];
const served: Promise<void>[] = [];
const remote: CodemodeRemote = {
execute(request: CodemodeRemoteRequest, exchange: CodemodeRemoteExchange) {
const done = serveCodemodeRemote(
structuredClone(request),
async (messages) => {
exchanges.push(messages.length);
return structuredClone(await exchange(structuredClone(messages)));
},
{ wasm, ...options },
);
served.push(done);
return done;
},
};
return { remote, exchanges, served };
}
describe("InlineCodemodeSandbox", () => {
it("fails a script that computes past its interrupt budget as a timeout", async () => {
const sandbox = new InlineCodemodeSandbox({ wasm, interruptBudget: 1_000 });
for (const code of ["while (true) {}", "while (true) await null"]) {
const result = await sandbox.execute(code, { timeoutMs: Number.POSITIVE_INFINITY });
expect(result).toMatchObject({
ok: false,
error: { kind: "timeout", message: "Execution exceeded its interrupt budget of 1000" },
});
}
});
it("counts the budget across waits on tools", async () => {
const sandbox = new InlineCodemodeSandbox({
wasm,
interruptBudget: 1_000,
tools: [{ name: "tick", execute: () => 1 }],
});
const result = await sandbox.execute("for (;;) { for (let i = 0; i < 1e5; i++); await tools.tick(); }");
expect(result).toMatchObject({ ok: false, error: { kind: "timeout" } });
expect(result.calls.length).toBeGreaterThan(1);
});
});
describe("RemoteCodemodeSandbox", () => {
it("runs tools in the caller and the script on the remote", async () => {
const { remote, served } = recordingRemote();
const seen: unknown[] = [];
const sandbox = new RemoteCodemodeSandbox({
remote: () => remote,
tools: [
{
name: "add",
execute: (args) => {
seen.push(args);
return (args as { a: number }).a + 1;
},
},
],
});
const result = await sandbox.execute("text('hi'); return await tools.add({ a: 1 })", { store: { k: 1 } });
expect(result).toMatchObject({ ok: true, value: 2, output: [{ type: "text", text: "hi" }] });
expect(seen).toEqual([{ a: 1 }]);
expect(result.calls).toMatchObject([{ name: "add", status: "ok" }]);
await Promise.all(served);
});
it("batches output instead of making one exchange per line", async () => {
const { remote, exchanges } = recordingRemote();
const sandbox = new RemoteCodemodeSandbox({ remote: () => remote });
const result = await sandbox.execute("for (let i = 0; i < 5000; i++) console.log(i); return 'ok'");
expect(result).toMatchObject({ ok: true, value: "ok" });
expect(result.output).toHaveLength(5000);
expect(result.output[4999]).toEqual({ type: "text", text: "4999", console: true });
expect(exchanges.length).toBeLessThanOrEqual(6);
});
it("caps the interrupt budget and memory at the remote's limits", async () => {
const { remote } = recordingRemote({ maxInterruptBudget: 1_000, maxMemoryLimitBytes: 8 * 1024 * 1024 });
const sandbox = new RemoteCodemodeSandbox({ remote: () => remote, interruptBudget: 1e9 });
expect(await sandbox.execute("while (true) {}")).toMatchObject({
ok: false,
error: { kind: "timeout", message: "Execution exceeded its interrupt budget of 1000" },
});
const memory = await sandbox.execute(
"const parts = []; for (;;) parts.push('x'.repeat(1 << 20) + parts.length);",
);
expect(memory).toMatchObject({ ok: false });
expect(JSON.stringify(memory)).toContain("out of memory");
});
it("tells the remote to stop when the caller aborts", async () => {
const { remote, served } = recordingRemote();
let started: () => void = () => {};
const call = new Promise<void>((resolve) => {
started = resolve;
});
const sandbox = new RemoteCodemodeSandbox({
remote: () => remote,
tools: [
{
name: "hang",
execute: () => {
started();
return new Promise(() => {});
},
},
],
});
const controller = new AbortController();
const pending = sandbox.execute("await tools.hang(); return 'never'", { signal: controller.signal });
await call;
controller.abort();
expect(await pending).toMatchObject({ ok: false, error: { kind: "aborted" } });
// The remote's pending exchange is answered with "stop", so its VM is disposed and execute() returns.
await Promise.all(served);
});
it("reports a failing or vanishing remote as a sandbox error", async () => {
const failing = new RemoteCodemodeSandbox({
remote: () => ({ execute: () => Promise.reject(new Error("object reset")) }),
});
expect(await failing.execute("return 1")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: "Remote sandbox failed: object reset" },
});
const silent = new RemoteCodemodeSandbox({ remote: () => ({ execute: () => Promise.resolve() }) });
expect(await silent.execute("return 1")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: "Remote sandbox returned before the script settled" },
});
const unreachable = new RemoteCodemodeSandbox({
remote: () => {
throw new Error("no binding");
},
});
expect(await unreachable.execute("return 1")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: "Failed to reach the remote sandbox: no binding" },
});
});
});

View File

@@ -0,0 +1,869 @@
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
CodemodeSandbox,
type CodemodeSandboxBase,
type CodemodeSandboxBaseOptions,
type CodemodeTool,
InlineCodemodeSandbox,
loadQuickJSWasm,
MAX_OUTPUT_CHARS,
MAX_OUTPUT_ITEMS,
RemoteCodemodeSandbox,
serveCodemodeRemote,
} from "../src/index.ts";
import { PRELUDE_SOURCE } from "../src/runtime/prelude-source.ts";
/**
* Every behavior test runs against each place a VM can run. The remote backend serves executions in
* process and structured-clones every message, as Workers RPC would. The in-thread backends get a
* small interrupt budget so runaway scripts end quickly.
*/
const INTERRUPT_BUDGET = 20_000;
const BACKENDS: Record<string, (options: CodemodeSandboxBaseOptions) => CodemodeSandboxBase> = {
worker: (options) => new CodemodeSandbox(options),
inline: (options) =>
new InlineCodemodeSandbox({ ...options, wasm: loadQuickJSWasm(), interruptBudget: INTERRUPT_BUDGET }),
remote: (options) =>
new RemoteCodemodeSandbox({
...options,
remote: () => ({
execute: (request, exchange) =>
serveCodemodeRemote(
structuredClone(request),
async (messages) => structuredClone(await exchange(structuredClone(messages))),
{ wasm: loadQuickJSWasm(), maxInterruptBudget: INTERRUPT_BUDGET },
),
}),
}),
};
let backend = "worker";
const sandboxes: CodemodeSandboxBase[] = [];
function newSandbox(options: CodemodeSandboxBaseOptions = {}): CodemodeSandboxBase {
const sandbox = BACKENDS[backend](options);
sandboxes.push(sandbox);
return sandbox;
}
function createSandbox(tools: CodemodeTool[] = [], timeoutMs = 10_000): CodemodeSandboxBase {
return newSandbox({ tools, timeoutMs });
}
afterEach(async () => {
await Promise.all(sandboxes.splice(0).map((sandbox) => sandbox.close()));
});
const echo: CodemodeTool = { name: "echo", execute: (args) => args };
// Base64 of the leading bytes of each format. image() only inspects the signature.
const PNG = "iVBORw0KGgo=";
const JPEG = "/9j/4A==";
const GIF = "R0lGODlh";
const WEBP = "UklGRgAAAABXRUJQ";
describe("embedded sources", () => {
it("parse as JavaScript", () => {
expect(() => new vm.Script(PRELUDE_SOURCE, { filename: "prelude.js" })).not.toThrow();
});
});
describe.each(Object.keys(BACKENDS))("%s backend", (name) => {
beforeEach(() => {
backend = name;
});
describe("script execution", () => {
it("returns the script's return value after a JSON round trip", async () => {
const sandbox = createSandbox();
expect(await sandbox.execute("return { a: 1, b: [true, 'x'] }")).toMatchObject({
ok: true,
value: { a: 1, b: [true, "x"] },
output: [],
calls: [],
});
expect(await sandbox.execute("return 'plain'")).toMatchObject({ ok: true, value: "plain" });
expect(await sandbox.execute("")).toMatchObject({ ok: true, value: undefined });
});
it("supports top-level await", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("const x = await Promise.resolve(41); return x + 1");
expect(result).toMatchObject({ ok: true, value: 42 });
});
it("collects text(), image(), and console output in order", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
console.log("hello", 1, { a: 1 });
text({ json: true });
text(undefined);
text(7);
image("data:image/png;base64,${PNG}");
image({ image_url: "data:image/jpeg;base64,${JPEG}" });
image({ type: "image", data: "${GIF}", mimeType: "image/gif" });
image("data:image/png;base64,${WEBP}");
image({ type: "image", data: "${PNG}" });
console.error(new Error("bad"));
return null;
`);
expect(result.ok).toBe(true);
expect(result.output.slice(0, -1)).toEqual([
{ type: "text", text: 'hello 1 {"a":1}', console: true },
{ type: "text", text: '{"json":true}' },
{ type: "text", text: "undefined" },
{ type: "text", text: "7" },
{ type: "image", data: PNG, mimeType: "image/png" },
{ type: "image", data: JPEG, mimeType: "image/jpeg" },
{ type: "image", data: GIF, mimeType: "image/gif" },
// The MIME type comes from the data, not from the declared type.
{ type: "image", data: WEBP, mimeType: "image/webp" },
{ type: "image", data: PNG, mimeType: "image/png" },
]);
expect(result.output.at(-1)).toMatchObject({
type: "text",
text: expect.stringMatching(/^Error: bad/),
console: true,
});
});
it("rejects invalid text() and image() arguments", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
const errors = [];
const circular = {};
circular.self = circular;
for (const run of [
() => text(circular),
() => image(""),
() => image("https://example.com/a.png"),
() => image("data:image/png,raw"),
() => image({ type: "text", text: "x" }),
() => image({ type: "image", data: "" }),
() => image(42),
() => image("data:image/png;base64,AAAA!"),
() => image("data:image/png;base64,AAAAA"),
() => image("data:image/png;base64,AA=A"),
() => image("data:image/png;base64,"),
() => image("data:image/png;base64,AAAA\\n[Output truncated]"),
() => image({ type: "image", data: "AAAA!", mimeType: "image/png" }),
() => image("data:image/png;base64,AAAA"),
() => image("data:image/png;base64,QUJD"),
() => image("data:image/jpeg;base64,/9j/9w=="),
]) {
try { run(); errors.push("no error"); } catch (error) { errors.push(error.name + ": " + error.message); }
}
return errors;
`);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.output).toEqual([]);
const errors = result.value as string[];
expect(errors[0]).toMatch(/^TypeError: .*circular/);
expect(errors.slice(1)).toEqual([
'TypeError: image expects a non-empty image URL string, an object with image_url, or an image block { type: "image", data, mimeType }',
"TypeError: remote image URLs are not supported in tool outputs. Pass a base64 data URI instead",
"TypeError: invalid image output. Pass a base64 data URI instead",
'TypeError: image only accepts image blocks, got "text"',
"TypeError: image expected image block data",
'TypeError: image expects a non-empty image URL string, an object with image_url, or an image block { type: "image", data, mimeType }',
...Array(6).fill(
"TypeError: invalid image output. The image data is not valid base64 (truncated or corrupted?)",
),
...Array(3).fill("TypeError: invalid image output. The image data is not a PNG, JPEG, GIF, or WebP image"),
]);
});
// https://github.com/earendil-works/pi/issues/10215
it("accepts wrapped and large base64 image data", async () => {
const sandbox = createSandbox();
const large = `iVBORw0KGgoA${"QUJD".repeat(256 * 1024)}`;
const result = await sandbox.execute(`
image("data:image/png;base64,iVBORw0K\\r\\nGgo=\\n");
image("data:image/png;base64,${large}");
`);
expect(result.ok).toBe(true);
expect(result.output).toEqual([
{ type: "image", data: PNG, mimeType: "image/png" },
{ type: "image", data: large, mimeType: "image/png" },
]);
});
it("ends the script successfully on exit(), keeping output and store writes", async () => {
const sandbox = createSandbox([echo]);
const result = await sandbox.execute(`
text("before");
store("k", 1);
await tools.echo(1);
try { exit(); } catch {}
text("after");
return "unreachable";
`);
expect(result).toMatchObject({
ok: true,
value: undefined,
output: [{ type: "text", text: "before" }],
storeWrites: { set: { k: 1 }, delete: [] },
});
});
it("keeps output produced before a failure", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute('text("partial");\nthrow new Error("boom")');
expect(result).toMatchObject({ ok: false, output: [{ type: "text", text: "partial" }] });
});
it("reports syntax errors with the script's line number", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("const a = 1;\nconst b = ;\nreturn a");
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.kind).toBe("script");
expect(result.error.name).toBe("SyntaxError");
expect(result.error.stack).toMatch(/codemode\.js:2/);
});
it("reports thrown errors with the script's line number", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("const a = 1;\nthrow new TypeError('boom ' + a)");
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatchObject({ kind: "script", name: "TypeError", message: "boom 1" });
expect(result.error.stack).toMatch(/codemode\.js:2/);
});
it("formats stacks like V8 without prelude frames", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("console.log(new Error('inner'));\nthrow new RangeError('outer')");
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.stack).toMatch(/^RangeError: outer\n {4}at .*codemode\.js:2/);
expect(result.error.stack).not.toContain("codemode-prelude.js");
expect(result.output[0]).toMatchObject({
type: "text",
text: expect.stringMatching(/^Error: inner\n {4}at .*codemode\.js:1/),
});
expect(JSON.stringify(result.output)).not.toContain("codemode-prelude.js");
});
it("reports non-Error throws", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("throw { code: 7 }");
expect(result).toMatchObject({ ok: false, error: { kind: "script", message: '{"code":7}' } });
});
it("reports a non-serializable return value as a script error", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("return 10n");
expect(result).toMatchObject({ ok: false, error: { kind: "script", name: "TypeError" } });
});
});
describe("tools", () => {
it("exposes tools as async functions and records calls", async () => {
const seen: unknown[] = [];
const sandbox = createSandbox([
{
name: "add",
execute: (args) => {
seen.push(args);
const { a, b } = args as { a: number; b: number };
return { sum: a + b };
},
},
]);
const result = await sandbox.execute(`
const first = await tools.add({ a: 1, b: 2 });
const second = await tools.add({ a: first.sum, b: 10 });
return second.sum;
`);
expect(result).toMatchObject({ ok: true, value: 13 });
expect(seen).toEqual([
{ a: 1, b: 2 },
{ a: 3, b: 10 },
]);
expect(result.calls.map((call) => [call.name, call.status])).toEqual([
["add", "ok"],
["add", "ok"],
]);
expect(result.calls.every((call) => call.durationMs >= 0)).toBe(true);
});
it("runs concurrent calls and lists tool names", async () => {
const sandbox = createSandbox([
echo,
{ name: "delay", execute: (args) => new Promise((resolve) => setTimeout(() => resolve(args), 20)) },
]);
const result = await sandbox.execute(`
const [a, b, c] = await Promise.all([tools.delay(1), tools.delay(2), tools.echo(3)]);
return { values: [a, b, c], names: Object.keys(tools) };
`);
expect(result).toMatchObject({ ok: true, value: { values: [1, 2, 3], names: ["echo", "delay"] } });
});
it("exposes tools under normalized identifiers and lists them in ALL_TOOLS", async () => {
const sandbox = createSandbox([
{ name: "my-tool", description: "Dashes", execute: () => "dash" },
{ name: "my_tool", description: "Shadowed", execute: () => "underscore" },
{ name: "mcp__docs__search", execute: () => "mcp" },
]);
const result = await sandbox.execute(`
try { ALL_TOOLS.push({}); } catch {}
return {
all: ALL_TOOLS,
calls: [await tools.my_tool(), await tools["my-tool"](), await tools.mcp__docs__search()],
};
`);
expect(result).toMatchObject({
ok: true,
value: {
all: [
{ name: "my_tool", description: "Dashes" },
{ name: "mcp__docs__search", description: "" },
],
calls: ["dash", "dash", "mcp"],
},
});
});
it("passes undefined arguments and results through", async () => {
const sandbox = createSandbox([{ name: "noop", execute: (args) => args }]);
const result = await sandbox.execute("return [await tools.noop(), await tools.noop(null)]");
expect(result).toMatchObject({ ok: true, value: [null, null] });
});
it("turns tool errors into catchable Errors in the script", async () => {
const sandbox = createSandbox([
{
name: "fail",
execute: () => {
throw new Error("tool exploded");
},
},
]);
const result = await sandbox.execute(`
try {
await tools.fail();
return "no error";
} catch (error) {
return { isError: error instanceof Error, message: error.message };
}
`);
expect(result).toMatchObject({ ok: true, value: { isError: true, message: "tool exploded" } });
expect(result.calls).toMatchObject([{ name: "fail", status: "error" }]);
});
it("rejects calls to unknown tools", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("return await tools.missing()");
expect(result).toMatchObject({ ok: false, error: { kind: "script", name: "TypeError" } });
});
it("aborts unawaited calls when the script returns", async () => {
let aborted = false;
const sandbox = createSandbox([
{
name: "slow",
execute: (_args, { signal }) =>
new Promise((_resolve, reject) => {
signal.addEventListener("abort", () => {
aborted = true;
reject(new Error("aborted"));
});
}),
},
]);
const result = await sandbox.execute("tools.slow(); return 'early'");
expect(result).toMatchObject({ ok: true, value: "early" });
expect(result.calls).toMatchObject([{ name: "slow", status: "cancelled" }]);
expect(aborted).toBe(true);
});
it("names close matches when a script reads a tool that does not exist", async () => {
const sandbox = createSandbox([echo, { name: "web-search", execute: () => "" }]);
const attempt = async (expression: string) => {
const result = await sandbox.execute(`return ${expression};`);
return result.ok ? result.value : result.error.message;
};
expect(await attempt("tools.Echo")).toBe(
'tools.Echo does not exist. Did you mean tools.echo? ALL_TOOLS lists every tool; searchTools(query) finds tools by topic. Check for a member with "Echo" in tools.',
);
expect(await attempt("tools.websearch")).toContain("Did you mean tools.web_search?");
expect(await attempt("tools.nothing")).toContain("Available: echo, web_search.");
expect(
await attempt("['echo' in tools, 'nothing' in tools, String(tools.toString), JSON.stringify(tools)]"),
).toEqual([true, false, "undefined", "{}"]);
});
it("supports register and unregister between executions", async () => {
const sandbox = createSandbox();
sandbox.registerTool(echo);
expect(() => sandbox.registerTool(echo)).toThrow(/already registered/);
expect(sandbox.tools.map((tool) => tool.name)).toEqual(["echo"]);
expect(await sandbox.execute("return await tools.echo('a')")).toMatchObject({ ok: true, value: "a" });
expect(sandbox.unregisterTool("echo")).toBe(true);
expect(await sandbox.execute("return 'echo' in tools")).toMatchObject({ ok: true, value: false });
});
});
describe("store and load", () => {
it("reads the snapshot and reports writes", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(
`
const seen = load("counter");
store("counter", seen + 1);
store("list", [1, { a: null }]);
store("old", undefined);
return [seen, load("counter"), load("missing"), load("old")];
`,
{ store: { counter: 41, old: "x" } },
);
expect(result).toMatchObject({
ok: true,
// undefined array elements become null in the JSON round trip of the return value.
value: [41, 42, null, null],
storeWrites: { set: { counter: 42, list: [1, { a: null }] }, delete: ["old"] },
});
});
it("returns copies, so mutating a loaded value does not change the store", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(
`const value = load("obj"); value.a = 2; const kept = { b: 1 }; store("kept", kept); kept.b = 2;
return [load("obj").a, load("kept").b];`,
{ store: { obj: { a: 1 } } },
);
expect(result).toMatchObject({ ok: true, value: [1, 1], storeWrites: { set: { kept: { b: 1 } } } });
});
it("rejects invalid keys, values, and oversized writes inside the script", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
const attempt = (fn) => { try { fn(); return "ok"; } catch (error) { return error.name; } };
return [
attempt(() => store(1, "x")),
attempt(() => load({})),
attempt(() => store("fn", () => 1)),
attempt(() => store("big", "x".repeat(300 * 1024))),
attempt(() => { for (let i = 0; i < 8; i++) store("k" + i, "x".repeat(200 * 1024)); }),
];
`);
expect(result).toMatchObject({
ok: true,
value: ["TypeError", "TypeError", "TypeError", "RangeError", "RangeError"],
});
});
it("explains oversized writes", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`store("img", "x".repeat(300 * 1024));`);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toContain('store("img") value has 307202 characters of JSON');
expect(result.error.message).toContain("Show images with image()");
});
it("reserves the store and load names", () => {
const execute = () => undefined;
expect(() => newSandbox({ globals: [{ name: "store", execute }] })).toThrow(/Invalid global/);
expect(() => newSandbox({ globals: [{ name: "load", execute }] })).toThrow(/Invalid global/);
});
});
describe("globals", () => {
it("exposes globals as top-level functions without recording them as calls", async () => {
const seen: unknown[] = [];
const sandbox = newSandbox({
tools: [echo],
globals: [{ name: "attach", execute: (args) => void seen.push(args) }],
});
const result = await sandbox.execute(`
await attach({ ref: 1 });
attach("not awaited");
return [typeof attach, typeof globalThis.attach, await tools.echo(2)];
`);
expect(result).toMatchObject({ ok: true, value: ["function", "function", 2] });
expect(result.calls.map((call) => call.name)).toEqual(["echo"]);
// Messages are handled in order, so an unawaited global still runs before the script settles.
expect(seen).toEqual([{ ref: 1 }, "not awaited"]);
});
it("groups namespaced globals and spreads arguments on request", async () => {
const seen: unknown[] = [];
const sandbox = newSandbox({
globals: [
{ name: "models.list", spread: true, execute: (args) => void seen.push(args) },
{ name: "models.first", execute: (args) => args },
],
});
const result = await sandbox.execute(`
await models.list("classifier", undefined, 3);
await models.list();
try { models.extra = 1; } catch {}
return [Object.keys(models), await models.first("a", "ignored"), "extra" in models];
`);
expect(result).toMatchObject({ ok: true, value: [["list", "first"], "a", false] });
// undefined array elements become null in the JSON round trip.
expect(seen).toEqual([["classifier", null, 3], []]);
});
it("names the members of a namespace when a script reads one that does not exist", async () => {
const execute = () => undefined;
const sandbox = newSandbox({
globals: [
{ name: "models.classify", execute },
{ name: "models.generateImages", execute },
],
});
const result = await sandbox.execute("await models.generateImage();");
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toBe(
'models.generateImage does not exist. Did you mean models.generateImages? Check for a member with "generateImage" in models.',
);
});
it("rejects invalid and reserved global names", () => {
const execute = () => undefined;
for (const name of ["a.b.c", "a.", ".a", "tools.x", "store.x", "a.not-valid"]) {
expect(() => newSandbox({ globals: [{ name, execute }] }), name).toThrow(/Invalid global/);
}
expect(() =>
newSandbox({
globals: [
{ name: "models", execute },
{ name: "models.list", execute },
],
}),
).toThrow(/conflicts with the namespace/);
expect(() => newSandbox({ globals: [{ name: "not-valid", execute }] })).toThrow(/Invalid global/);
expect(() => newSandbox({ globals: [{ name: "tools", execute }] })).toThrow(/Invalid global/);
expect(() => newSandbox({ globals: [{ name: "console", execute }] })).toThrow(/Invalid global/);
});
});
describe("limits and lifetime", () => {
it("terminates a synchronous infinite loop on timeout", async () => {
const sandbox = createSandbox();
const started = performance.now();
const result = await sandbox.execute("while (true) {}", { timeoutMs: 200 });
expect(result).toMatchObject({ ok: false, error: { kind: "timeout" } });
expect(performance.now() - started).toBeLessThan(5_000);
});
it("runs without a deadline when timeoutMs is Infinity", async () => {
const sandbox = createSandbox([
{ name: "wait", execute: () => new Promise((r) => setTimeout(() => r("late"), 50)) },
]);
const result = await sandbox.execute("return await tools.wait()", { timeoutMs: Number.POSITIVE_INFINITY });
expect(result).toMatchObject({ ok: true, value: "late" });
});
it("fails a script that waits on a promise nothing can settle", async () => {
const sandbox = createSandbox([{ name: "echo", execute: (args) => args }]);
const result = await sandbox.execute("await tools.echo(1); await new Promise(() => {}); return 'never'", {
timeoutMs: Number.POSITIVE_INFINITY,
});
expect(result).toMatchObject({
ok: false,
error: { kind: "script", message: expect.stringContaining("can never settle") },
});
// Returning while a call is still pending is not a stall.
const returned = await sandbox.execute("tools.echo(2); return 'early'", {
timeoutMs: Number.POSITIVE_INFINITY,
});
expect(returned).toMatchObject({ ok: true, value: "early" });
});
it("terminates a microtask-spinning loop on timeout", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("while (true) await null", { timeoutMs: 200 });
expect(result).toMatchObject({ ok: false, error: { kind: "timeout" } });
});
it("aborts via signal and cancels in-flight calls", async () => {
const controller = new AbortController();
let toolSignal: AbortSignal | undefined;
// Resolves on each call, so the test waits for the worker instead of a fixed delay.
let called: () => void = () => {};
const nextCall = () =>
new Promise<void>((resolve) => {
called = resolve;
});
const sandbox = createSandbox([
{
name: "hang",
execute: (_args, { signal }) => {
toolSignal = signal;
called();
return new Promise(() => {});
},
},
]);
const firstCall = nextCall();
const promise = sandbox.execute("await tools.hang(); return 'never'");
await firstCall;
controller.abort(new Error("user cancelled"));
expect(toolSignal?.aborted).toBe(false);
const result = await sandbox.execute("await tools.hang()", { signal: controller.signal });
expect(result).toMatchObject({ ok: false, error: { kind: "aborted", message: "user cancelled" } });
const second = new AbortController();
const secondCall = nextCall();
const pending = sandbox.execute("await tools.hang(); return 'never'", { signal: second.signal });
await secondCall;
second.abort();
const aborted = await pending;
expect(aborted).toMatchObject({ ok: false, error: { kind: "aborted" } });
expect(aborted.calls).toMatchObject([{ name: "hang", status: "cancelled" }]);
expect(toolSignal?.aborted).toBe(true);
await sandbox.close();
expect(await promise).toMatchObject({ ok: false, error: { kind: "aborted", message: "Sandbox closed" } });
});
// #10283: the host keeps all output, so a script that prints in a loop must not grow it without bound.
it("fails a script whose output passes the limits, even if it catches the error", async () => {
const sandbox = createSandbox();
for (const print of ["text(s)", "console.log(s)", 'image("data:image/png;base64," + p)']) {
const result = await sandbox.execute(`
const s = "x".repeat(1 << 20);
const p = "iVBORw0KGgoA" + "A".repeat(1 << 20);
for (;;) { try { ${print}; } catch {} }
`);
expect(result).toMatchObject({
ok: false,
error: {
kind: "script",
name: "RangeError",
message: expect.stringContaining("script output exceeded"),
},
});
const chars = result.output.reduce(
(sum, item) => sum + (item.type === "text" ? item.text.length : item.data.length),
0,
);
expect(chars).toBeLessThanOrEqual(MAX_OUTPUT_CHARS);
expect(chars).toBeGreaterThan(MAX_OUTPUT_CHARS - (2 << 20));
}
const empty = await sandbox.execute(`for (;;) text("");`);
expect(empty).toMatchObject({ ok: false, error: { name: "RangeError" } });
expect(empty.output).toHaveLength(MAX_OUTPUT_ITEMS);
});
it("rejects execute after close", async () => {
const sandbox = createSandbox();
await sandbox.close();
await expect(sandbox.execute("return 1")).rejects.toThrow(/closed/);
});
it("runs executions in parallel without sharing state", async () => {
const sandbox = createSandbox();
const results = await Promise.all([
sandbox.execute("globalThis.shared = 'a'; await null; return globalThis.shared"),
sandbox.execute("globalThis.shared = 'b'; await null; return globalThis.shared"),
sandbox.execute("return typeof globalThis.shared"),
]);
expect(results.map((result) => (result.ok ? result.value : result.error))).toEqual(["a", "b", "undefined"]);
});
it("turns deep recursion into a catchable RangeError", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
let depth = 0;
function dive() { depth++; dive(); }
try { dive(); } catch (error) { return [error.name, depth > 1000]; }
`);
expect(result).toMatchObject({ ok: true, value: ["RangeError", true] });
});
});
describe("escape hatches", () => {
it("has no host globals", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
return [
typeof process, typeof require, typeof module, typeof setTimeout, typeof fetch,
typeof WebAssembly, typeof std, typeof os, typeof globalThis.constructor,
]
`);
expect(result).toMatchObject({
ok: true,
value: [
"undefined",
"undefined",
"undefined",
"undefined",
"undefined",
"undefined",
"undefined",
"undefined",
"function",
],
});
});
it("keeps eval and Function inside the VM", async () => {
// Code generation is allowed: it can only produce more code in the same wasm instance.
const sandbox = createSandbox([echo]);
const result = await sandbox.execute(`
return [
eval("typeof process"),
new Function("return typeof process")(),
tools.echo.constructor("return typeof require")(),
(async () => {}).constructor("return typeof setTimeout")() instanceof Promise,
];
`);
expect(result).toMatchObject({ ok: true, value: ["undefined", "undefined", "undefined", true] });
});
it("rejects dynamic import", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
try { await import("node:fs"); return "imported"; } catch (error) { return error.constructor.name; }
`);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value).not.toBe("imported");
});
// #10444: the prelude shares the built-ins with the script, so patching them could corrupt what
// the prelude sends to the host.
it("ignores patches to built-ins and built-in globals", async () => {
const sandbox = createSandbox([echo]);
const result = await sandbox.execute(`
Array.prototype.toJSON = () => null;
Object.prototype.toJSON = () => 5;
Promise.prototype.then = () => {};
Map.prototype.get = () => undefined;
globalThis.JSON = { stringify: () => "x", parse: () => "x" };
store("k", [1]);
return [await tools.echo([2]), JSON.stringify({ a: 1 })];
`);
expect(result).toMatchObject({ ok: true, value: [[2], '{"a":1}'], storeWrites: { set: { k: [1] } } });
});
it("freezes intrinsics that are only reachable from instances", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
return [
Object.getPrototypeOf(function* () {}).prototype,
Object.getPrototypeOf(async function () {}),
Object.getPrototypeOf(Int8Array).prototype,
Object.getPrototypeOf([][Symbol.iterator]()),
Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())),
Object.getPrototypeOf(new Map()[Symbol.iterator]()),
Object.getPrototypeOf(/a/[Symbol.matchAll]("")),
].every((object) => Object.isFrozen(object));
`);
expect(result).toMatchObject({ ok: true, value: true });
});
it("still lets instances override properties of frozen prototypes", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute(`
const object = {};
object.toString = () => "custom";
function Legacy() {}
Legacy.prototype = Object.create(Error.prototype);
Legacy.prototype.constructor = Legacy;
const bare = new Error();
bare.message = "set later";
class MyError extends Error {
constructor(message) {
super(message);
this.name = "MyError";
}
}
let patched = "silent";
try { Error.prototype.name = "Patched"; } catch (error) { patched = error.constructor.name; }
return [String(object), new Legacy().constructor === Legacy, bare.message, new MyError("x").name, Error.prototype.name, patched];
`);
expect(result).toMatchObject({
ok: true,
value: ["custom", true, "set later", "MyError", "Error", "TypeError"],
});
});
it("reports errors whose name or message is not a string", async () => {
const sandbox = createSandbox();
const result = await sandbox.execute("const error = new Error('x'); error.message = 42; throw error;");
expect(result).toMatchObject({ ok: false, error: { kind: "script", name: "Error", message: "42" } });
});
it("keeps tools and console frozen", async () => {
const sandbox = createSandbox([echo]);
const result = await sandbox.execute(`
try { tools.echo = () => 'nope'; } catch {}
try { tools.extra = () => 'nope'; } catch {}
try { globalThis.tools = null; } catch {}
return ["extra" in tools, await tools.echo('still')];
`);
expect(result).toMatchObject({ ok: true, value: [false, "still"] });
});
});
});
describe("worker host", () => {
beforeEach(() => {
backend = "worker";
});
it("accepts a worker path string", async () => {
const workerPath = fileURLToPath(new URL("../src/runtime/worker.ts", import.meta.url));
const sandbox = new CodemodeSandbox({ workerUrl: workerPath });
sandboxes.push(sandbox);
expect(await sandbox.execute("return 1")).toMatchObject({ ok: true, value: 1 });
});
it("reports a missing worker file as a sandbox error", async () => {
const sandbox = new CodemodeSandbox({ workerUrl: new URL("./does-not-exist.js", import.meta.url) });
sandboxes.push(sandbox);
expect(await sandbox.execute("return 1")).toMatchObject({ ok: false, error: { kind: "sandbox" } });
});
it("reports a failing wasm module as a sandbox error", async () => {
const sandbox = new CodemodeSandbox({ wasm: Promise.reject(new Error("no wasm")) });
sandboxes.push(sandbox);
expect(await sandbox.execute("return 1")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: "Failed to load QuickJS: no wasm" },
});
});
// #10725: Node 24/26 `--watch` posts dependency reports on the worker channel.
it("ignores Node --watch notifications from the worker", async () => {
const sandbox = new CodemodeSandbox({ workerUrl: new URL("./fixtures/raw-worker.ts", import.meta.url) });
sandboxes.push(sandbox);
const result = await sandbox.execute(
JSON.stringify([
{ "watch:require": ["/worker.js"] },
{ "watch:import": ["file:///worker.js"] },
{ type: "done", ok: true, value: "42", writes: "[]" },
]),
);
expect(result).toMatchObject({ ok: true, value: 42 });
});
// #10444: a malformed payload from the worker must fail the run as a sandbox error instead of
// throwing in the host's message listener and never settling.
it.each([
[{ type: "done", ok: true, value: "1", writes: "null" }, "store writes are not an array"],
[{ type: "done", ok: true, value: "1", writes: "[1]" }, "store writes contain a malformed entry"],
[{ type: "done", ok: true, value: "1", writes: '[["k", "{"]]' }, 'store value for "k" is not valid JSON'],
[{ type: "done", ok: true, value: "{", writes: "[]" }, "return value is not valid JSON"],
[{ type: "done", ok: false, error: "5" }, "script error is not an object"],
[{ type: "done", ok: false, error: "{}" }, "script error is malformed"],
[{ type: "nonsense" }, "unknown message from the worker"],
])("reports a broken bridge as a sandbox error: %j", async (message, reason) => {
const sandbox = new CodemodeSandbox({ workerUrl: new URL("./fixtures/raw-worker.ts", import.meta.url) });
sandboxes.push(sandbox);
const result = await sandbox.execute(JSON.stringify(message));
expect(result).toMatchObject({ ok: false, error: { kind: "sandbox" } });
expect(result.ok ? undefined : result.error.message).toContain(`Sandbox bridge broken: ${reason}`);
});
});

View File

@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import { CodemodeSourceError, parseCodemodeSource } from "../src/source.ts";
describe("parseCodemodeSource", () => {
it("returns plain code unchanged", () => {
expect(parseCodemodeSource("text('hi')")).toEqual({ code: "text('hi')", options: {} });
expect(parseCodemodeSource("// just a comment\nreturn 1")).toEqual({
code: "// just a comment\nreturn 1",
options: {},
});
});
it("parses the options line and keeps line numbers", () => {
expect(parseCodemodeSource('// @options: {"timeout_ms": 10}\nconst a = 1;\ntext(a)')).toEqual({
code: "\nconst a = 1;\ntext(a)",
options: { timeoutMs: 10 },
});
expect(parseCodemodeSource(' // @options:{"max_output_tokens":0,"timeout_ms":1500}\r\ntext(1)').options).toEqual(
{
maxOutputTokens: 0,
timeoutMs: 1500,
},
);
expect(parseCodemodeSource("// @options: {}\ntext(1)")).toEqual({ code: "\ntext(1)", options: {} });
});
it("only treats the first line as an options line", () => {
expect(parseCodemodeSource("// @optionsx {}\ntext(1)").options).toEqual({});
expect(parseCodemodeSource('text("// @options: {}")').options).toEqual({});
});
// #10631: an options line after the first line used to be ignored, so its timeout never applied.
it("rejects an options line that is not the first line", () => {
const cases: [string, number][] = [
['// count things\n// @options: {"timeout_ms": 1000}\nwhile (true) {}', 2],
['\n // @options: {"timeout_ms": 1000}\nwhile (true) {}', 2],
['text(1)\r\n// @options: {"timeout_ms": 1}', 2],
['// @options: {"timeout_ms": 1}\ntext(1)\n// @options: {"timeout_ms": 2}', 3],
];
for (const [input, line] of cases) {
expect(() => parseCodemodeSource(input), input).toThrow(CodemodeSourceError);
expect(() => parseCodemodeSource(input), input).toThrow(
`The // @options: line must be the first line of the input; found one on line ${line}`,
);
}
});
it("rejects empty input and invalid options", () => {
const cases: [string, string | RegExp][] = [
["", /Expected JavaScript source text \(non-empty\)/],
[" \n", /Expected JavaScript source text \(non-empty\)/],
["// @options:\ntext(1)", /@options must be a JSON object with supported fields/],
["// @options: {timeout_ms: 1}\ntext(1)", /@options must be valid JSON with supported fields/],
["// @options: [1]\ntext(1)", /@options must be a JSON object with supported fields/],
[
'// @options: {"yield": 1}\ntext(1)',
"@options only supports `max_output_tokens` and `timeout_ms`; got `yield`",
],
[
'// @options: {"max_output_tokens": 1.5}\ntext(1)',
"@options field `max_output_tokens` must be a non-negative safe integer",
],
['// @options: {"timeout_ms": 0}\ntext(1)', /@options field `timeout_ms` must be a positive integer/],
[
'// @options: {"timeout_ms": 1}',
"The @options line must be followed by JavaScript source on subsequent lines",
],
[
'// @options: {"timeout_ms": 1}\n \n',
"The @options line must be followed by JavaScript source on subsequent lines",
],
];
for (const [input, message] of cases) {
expect(() => parseCodemodeSource(input), input).toThrow(CodemodeSourceError);
expect(() => parseCodemodeSource(input), input).toThrow(message);
}
});
});

View File

@@ -0,0 +1,229 @@
import { execFile, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Worker } from "node:worker_threads";
import { afterEach, describe, expect, it, vi } from "vitest";
import { type CodemodeResult, CodemodeSandbox } from "../src/index.ts";
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("worker lifetime", () => {
// #10748: reject admission before creating a worker, not by terminating an excess worker.
it("does not start a worker when admission is denied", async () => {
const onWorkerStarted = vi.fn();
const acquireWorker = vi.fn(() => {
throw new Error("worker capacity exhausted");
});
const sandbox = new CodemodeSandbox({ acquireWorker, onWorkerStarted });
try {
expect(await sandbox.execute("while (true) {}")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: "worker capacity exhausted" },
});
expect(acquireWorker).toHaveBeenCalledTimes(1);
expect(onWorkerStarted).not.toHaveBeenCalled();
} finally {
await sandbox.close();
}
});
// #10748: a failed Worker constructor must return its reserved capacity.
it("releases admission when worker construction fails", async () => {
const releaseWorker = vi.fn();
const acquireWorker = vi.fn(() => releaseWorker);
const sandbox = new CodemodeSandbox({
acquireWorker,
workerUrl: new URL("https://invalid.example/worker.js"),
});
try {
expect(await sandbox.execute("return 1")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: expect.stringContaining("Failed to start worker:") },
});
expect(acquireWorker).toHaveBeenCalledTimes(1);
expect(releaseWorker).toHaveBeenCalledTimes(1);
} finally {
await sandbox.close();
}
});
// #10748: loading failures must not reserve worker capacity.
it("does not acquire admission when wasm loading fails", async () => {
const acquireWorker = vi.fn(() => vi.fn());
const sandbox = new CodemodeSandbox({
acquireWorker,
wasm: Promise.reject(new Error("wasm unavailable")),
});
try {
expect(await sandbox.execute("return 1")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: "Failed to load QuickJS: wasm unavailable" },
});
expect(acquireWorker).not.toHaveBeenCalled();
} finally {
await sandbox.close();
}
});
// #10748: Bun can leave terminate() pending while a non-interruptible built-in runs.
it.each(["abort", "timeout", "close"] as const)("settles %s without waiting for worker exit", async (how) => {
vi.useFakeTimers();
const terminate = Worker.prototype.terminate;
let worker: Worker | undefined;
let releaseTermination: (code: number) => void = () => {};
const termination = new Promise<number>((resolve) => {
releaseTermination = resolve;
});
const terminateSpy = vi.spyOn(Worker.prototype, "terminate").mockImplementation(function (this: Worker) {
worker = this;
// Like the real Node/Bun implementation, termination refs the worker while waiting for exit.
this.ref();
return termination;
});
const refSpy = vi.spyOn(Worker.prototype, "ref");
const unrefSpy = vi.spyOn(Worker.prototype, "unref");
let ready: () => void = () => {};
const called = new Promise<void>((resolve) => {
ready = resolve;
});
let toolSignal: AbortSignal | undefined;
let exited: Promise<void> | undefined;
let didExit = false;
const sandbox = new CodemodeSandbox({
timeoutMs: 100,
onWorkerStarted: (promise) => {
exited = promise;
void promise.then(() => {
didExit = true;
});
},
tools: [
{
name: "hang",
execute: (_args, { signal }) => {
toolSignal = signal;
ready();
return new Promise(() => {});
},
},
],
});
const controller = new AbortController();
let result: CodemodeResult | undefined;
let didClose = false;
const pending = sandbox.execute('text("partial"); await tools.hang()', { signal: controller.signal });
void pending.then((value) => {
result = value;
});
try {
await called;
if (how === "abort") controller.abort(new Error("cancelled"));
if (how === "timeout") await vi.advanceTimersByTimeAsync(100);
if (how === "close") {
void sandbox.close().then(() => {
didClose = true;
});
}
await vi.advanceTimersByTimeAsync(0);
expect(result).toMatchObject({
ok: false,
error: { kind: how === "timeout" ? "timeout" : "aborted" },
output: [{ type: "text", text: "partial" }],
calls: [{ name: "hang", status: "cancelled" }],
});
expect(toolSignal?.aborted).toBe(true);
expect(terminateSpy).toHaveBeenCalledTimes(1);
expect(unrefSpy).toHaveBeenCalledTimes(1);
expect(refSpy).toHaveBeenCalledTimes(1);
expect(unrefSpy.mock.invocationCallOrder[0]).toBeGreaterThan(refSpy.mock.invocationCallOrder[0]);
expect(exited).toBeDefined();
expect(didExit).toBe(false);
if (how === "close") expect(didClose).toBe(true);
// Neither close() nor late worker messages may replace the decided outcome or wait for exit.
await sandbox.close();
worker?.emit("message", { type: "done", ok: true, value: "42", writes: "[]" });
expect(await pending).toBe(result);
expect(terminateSpy).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
terminateSpy.mockRestore();
if (worker) releaseTermination(await terminate.call(worker));
await sandbox.close();
}
await exited;
expect(didExit).toBe(true);
});
// #10748: exit accounting applies to successful executions and host-controlled runs too.
it("reports the actual exit of execute() and start() workers", async () => {
const exits: Promise<void>[] = [];
const releaseWorker = vi.fn();
const acquireWorker = vi.fn(() => releaseWorker);
const sandbox = new CodemodeSandbox({ acquireWorker, onWorkerStarted: (exited) => void exits.push(exited) });
try {
expect(await sandbox.execute("return 1")).toMatchObject({ ok: true, value: 1 });
const run = sandbox.start("return 2", { startedAt: 0, seed: 0, onCall: () => {} });
expect(await run.result).toMatchObject({ ok: true, value: 2 });
expect(run.finished).toBe(true);
expect(exits).toHaveLength(2);
await Promise.all(exits);
expect(acquireWorker).toHaveBeenCalledTimes(2);
expect(releaseWorker).toHaveBeenCalledTimes(2);
} finally {
await sandbox.close();
}
});
// #10748: a failing lifetime hook must not leak the worker it was handed.
it("requests termination when onWorkerStarted throws", async () => {
let exited: Promise<void> | undefined;
const releaseWorker = vi.fn();
const sandbox = new CodemodeSandbox({
acquireWorker: () => releaseWorker,
onWorkerStarted: (promise) => {
exited = promise;
throw new Error("lifetime hook failed");
},
});
try {
expect(await sandbox.execute("while (true) {}")).toMatchObject({
ok: false,
error: { kind: "sandbox", message: "lifetime hook failed" },
});
expect(exited).toBeDefined();
await exited;
expect(releaseWorker).toHaveBeenCalledTimes(1);
} finally {
await sandbox.close();
}
});
});
const runtimes = [
{ name: "Node", command: process.execPath, available: true },
{ name: "Bun", command: "bun", available: spawnSync("bun", ["--version"], { timeout: 5000 }).status === 0 },
];
describe.each(runtimes)("$name process lifetime", ({ name, command, available }) => {
// #10748: verify both prompt cancellation and, on Node, prompt process exit with a real worker.
// Bun 1.4.0's pending terminate() keeps the process alive even after unref(), independently of codemode.
it.skipIf(!available).each(["abort", "timeout", "close"] as const)(
name === "Node" ? "exits promptly after %s with a real worker" : "settles %s promptly with a real worker",
async (how) => {
const fixture = fileURLToPath(new URL("./fixtures/worker-process-exit.ts", import.meta.url));
const { stdout } = await promisify(execFile)(command, [fixture, how], { timeout: 20000 });
const lines = stdout.trim().split(/\r?\n/);
expect(lines).toHaveLength(2);
const result = JSON.parse(lines[0]) as { type: string; kind: string; elapsedMs: number };
const exit = JSON.parse(lines[1]) as { type: string; elapsedMs: number };
expect(result).toMatchObject({ type: "result", kind: how === "timeout" ? "timeout" : "aborted" });
expect(exit.type).toBe("exit");
expect(result.elapsedMs).toBeLessThan(1000);
if (name === "Node") expect(exit.elapsedMs - result.elapsedMs).toBeLessThan(1000);
},
25000,
);
});