branch:
executor.test.ts
13858 bytesRaw
/**
* Tests for the Executor interface contract and DynamicWorkerExecutor.
*
* Uses vitest-pool-workers — tests run inside a real Workers runtime
* with a real WorkerLoader binding, no mocks needed.
*/
import { describe, it, expect, vi } from "vitest";
import { env } from "cloudflare:workers";
import {
DynamicWorkerExecutor,
ToolDispatcher,
type ResolvedProvider
} from "../executor";
type ToolFns = Record<string, (...args: unknown[]) => Promise<unknown>>;
/** Helper to wrap raw fns into the default "codemode" provider. */
function codemodeProvider(fns: ToolFns): ResolvedProvider {
return { name: "codemode", fns };
}
describe("ToolDispatcher", () => {
it("should dispatch tool calls and return JSON result", async () => {
const double = vi.fn(async (...args: unknown[]) => {
const input = args[0] as Record<string, unknown>;
return { doubled: (input.n as number) * 2 };
});
const fns: ToolFns = { double };
const dispatcher = new ToolDispatcher(fns);
const resJson = await dispatcher.call("double", JSON.stringify({ n: 5 }));
const data = JSON.parse(resJson);
expect(data.result).toEqual({ doubled: 10 });
expect(double).toHaveBeenCalledWith({ n: 5 });
});
it("should return error for unknown tool", async () => {
const dispatcher = new ToolDispatcher({});
const resJson = await dispatcher.call("nonexistent", "{}");
const data = JSON.parse(resJson);
expect(data.error).toContain("nonexistent");
});
it("should return error when tool function throws", async () => {
const fns: ToolFns = {
broken: async () => {
throw new Error("something broke");
}
};
const dispatcher = new ToolDispatcher(fns);
const resJson = await dispatcher.call("broken", "{}");
const data = JSON.parse(resJson);
expect(data.error).toBe("something broke");
});
it("should handle empty args string", async () => {
const noArgs = vi.fn(async () => "ok");
const fns: ToolFns = { noArgs };
const dispatcher = new ToolDispatcher(fns);
const resJson = await dispatcher.call("noArgs", "");
const data = JSON.parse(resJson);
expect(data.result).toBe("ok");
expect(noArgs).toHaveBeenCalledWith({});
});
});
describe("DynamicWorkerExecutor", () => {
it("should execute simple code that returns a value", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute("async () => 42", [
codemodeProvider({})
]);
expect(result.result).toBe(42);
expect(result.error).toBeUndefined();
});
it("should call tool functions via codemode proxy", async () => {
const add = vi.fn(async (...args: unknown[]) => {
const input = args[0] as Record<string, unknown>;
return (input.a as number) + (input.b as number);
});
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
"async () => await codemode.add({ a: 3, b: 4 })",
[codemodeProvider({ add })]
);
expect(result.result).toBe(7);
expect(add).toHaveBeenCalledWith({ a: 3, b: 4 });
});
it("should handle multiple sequential tool calls", async () => {
const getWeather = vi.fn(async () => ({ temp: 72 }));
const searchWeb = vi.fn(async (...args: unknown[]) => {
const input = args[0] as Record<string, unknown>;
return { results: [`news about ${input.query as string}`] };
});
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const code = `async () => {
const weather = await codemode.getWeather({});
const news = await codemode.searchWeb({ query: "temp " + weather.temp });
return { weather, news };
}`;
const result = await executor.execute(code, [
codemodeProvider({ getWeather, searchWeb })
]);
expect(result.result).toEqual({
weather: { temp: 72 },
news: { results: ["news about temp 72"] }
});
expect(getWeather).toHaveBeenCalledTimes(1);
expect(searchWeb).toHaveBeenCalledTimes(1);
});
it("should return error when code throws", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
'async () => { throw new Error("boom"); }',
[codemodeProvider({})]
);
expect(result.error).toBe("boom");
});
it("should return error when tool function throws", async () => {
const fail = vi.fn(async () => {
throw new Error("tool error");
});
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
"async () => await codemode.fail({})",
[codemodeProvider({ fail })]
);
expect(result.error).toBe("tool error");
});
it("should handle concurrent tool calls via Promise.all", async () => {
const slow = async (...args: unknown[]) => {
const input = args[0] as Record<string, unknown>;
return { id: input.id as number };
};
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const code = `async () => {
const [a, b, c] = await Promise.all([
codemode.slow({ id: 1 }),
codemode.slow({ id: 2 }),
codemode.slow({ id: 3 })
]);
return [a, b, c];
}`;
const result = await executor.execute(code, [codemodeProvider({ slow })]);
expect(result.result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
});
it("should capture console.log output", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
'async () => { console.log("hello"); console.warn("careful"); return "done"; }',
[codemodeProvider({})]
);
expect(result.result).toBe("done");
expect(result.logs).toContain("hello");
expect(result.logs).toContain("[warn] careful");
});
it("should handle code containing backticks and template literals", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
'async () => { return `hello ${"world"}`; }',
[codemodeProvider({})]
);
expect(result.result).toBe("hello world");
});
it("should block external fetch by default (globalOutbound: null)", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
'async () => { const r = await fetch("https://example.com"); return r.status; }',
[codemodeProvider({})]
);
// fetch should fail because globalOutbound defaults to null
expect(result.error).toBeDefined();
});
it("should preserve closures in tool functions", async () => {
const secret = "api-key-123";
const fns: ToolFns = {
getSecret: async () => ({ key: secret })
};
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
"async () => await codemode.getSecret({})",
[codemodeProvider(fns)]
);
expect(result.result).toEqual({ key: "api-key-123" });
});
it("should make custom modules importable in sandbox code", async () => {
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
modules: {
"helpers.js": 'export function greet(name) { return "hello " + name; }'
}
});
const code = `async () => {
const { greet } = await import("helpers.js");
return greet("world");
}`;
const result = await executor.execute(code, [codemodeProvider({})]);
expect(result.result).toBe("hello world");
expect(result.error).toBeUndefined();
});
it("should not allow custom modules to override executor.js", async () => {
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
modules: {
"executor.js": "export default class Evil {}"
}
});
// Should still work normally — the reserved key is ignored
const result = await executor.execute("async () => 1 + 1", [
codemodeProvider({})
]);
expect(result.result).toBe(2);
expect(result.error).toBeUndefined();
});
it("should normalize code automatically (strip fences, wrap expressions)", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
// Code wrapped in markdown fences — should be stripped and normalized
const result = await executor.execute("```js\n1 + 1\n```", [
codemodeProvider({})
]);
expect(result.result).toBe(2);
expect(result.error).toBeUndefined();
});
it("should normalize bare expressions into async arrow functions", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute("42", [codemodeProvider({})]);
expect(result.result).toBe(42);
expect(result.error).toBeUndefined();
});
it("should work with empty providers array", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute("async () => 42", []);
expect(result.result).toBe(42);
expect(result.error).toBeUndefined();
});
it("should sanitize tool names with hyphens and dots", async () => {
const listIssues = vi.fn(async () => [{ id: 1, title: "bug" }]);
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
// Note: sanitization happens in createCodeTool (tool.ts), not the executor.
// The executor receives pre-sanitized names.
const result = await executor.execute(
"async () => await codemode.github_list_issues({})",
[codemodeProvider({ github_list_issues: listIssues })]
);
expect(result.result).toEqual([{ id: 1, title: "bug" }]);
expect(listIssues).toHaveBeenCalledWith({});
});
it("should include timeout in execution", async () => {
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
timeout: 100
});
const result = await executor.execute(
"async () => { await new Promise(r => setTimeout(r, 5000)); return 'done'; }",
[codemodeProvider({})]
);
expect(result.error).toContain("timed out");
});
});
// ── Multiple provider tests ──────────────────────────────────────────
describe("Multiple providers (namespaces)", () => {
it("exposes tools under a named provider namespace", async () => {
const echo = vi.fn(async (args: unknown) => {
const { msg } = args as { msg: string };
return { echoed: msg };
});
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
`async () => await myns.echo({ msg: "hello" })`,
[{ name: "myns", fns: { echo } }]
);
expect(result.error).toBeUndefined();
expect(result.result).toEqual({ echoed: "hello" });
expect(echo).toHaveBeenCalledWith({ msg: "hello" });
});
it("multiple providers each get their own namespace", async () => {
const storeGet = vi.fn(async () => ({ from: "store" }));
const cacheGet = vi.fn(async () => ({ from: "cache" }));
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
`async () => {
const a = await store.get({});
const b = await cache.get({});
return { a, b };
}`,
[
{ name: "store", fns: { get: storeGet } },
{ name: "cache", fns: { get: cacheGet } }
]
);
expect(result.error).toBeUndefined();
expect(result.result).toEqual({
a: { from: "store" },
b: { from: "cache" }
});
expect(storeGet).toHaveBeenCalledTimes(1);
expect(cacheGet).toHaveBeenCalledTimes(1);
});
it("named provider and codemode.* coexist in the same sandbox", async () => {
const addFn = vi.fn(async (args: unknown) => {
const { a, b } = args as { a: number; b: number };
return a + b;
});
const echoFn = vi.fn(async (args: unknown) => {
const { msg } = args as { msg: unknown };
return { echoed: msg };
});
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
`async () => {
const sum = await codemode.add({ a: 3, b: 4 });
const pong = await echo.ping({ msg: sum });
return { sum, pong };
}`,
[
{ name: "codemode", fns: { add: addFn } },
{ name: "echo", fns: { ping: echoFn } }
]
);
expect(result.error).toBeUndefined();
expect(result.result).toEqual({
sum: 7,
pong: { echoed: 7 }
});
expect(addFn).toHaveBeenCalledWith({ a: 3, b: 4 });
});
it("provider errors propagate as sandbox errors", async () => {
const failing = async () => {
throw new Error("provider failed");
};
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
`async () => await broken.doSomething({})`,
[{ name: "broken", fns: { doSomething: failing } }]
);
expect(result.error).toBe("provider failed");
});
it("rejects duplicate provider names", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute("async () => 1", [
{ name: "dup", fns: {} },
{ name: "dup", fns: {} }
]);
expect(result.error).toContain("Duplicate");
});
it("rejects reserved provider names", async () => {
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute("async () => 1", [
{ name: "__dispatchers", fns: {} }
]);
expect(result.error).toContain("reserved");
});
});