/** * 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 Promise>; /** 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; 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; 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; 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; 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"); }); });