@cloudflare/codemode 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Tests for the Executor interface contract and DynamicWorkerExecutor.
3
+ *
4
+ * Uses vitest-pool-workers — tests run inside a real Workers runtime
5
+ * with a real WorkerLoader binding, no mocks needed.
6
+ */
7
+ import { describe, it, expect, vi } from "vitest";
8
+ import { env } from "cloudflare:test";
9
+ import { DynamicWorkerExecutor, ToolDispatcher } from "../executor";
10
+
11
+ type ToolFns = Record<string, (...args: unknown[]) => Promise<unknown>>;
12
+
13
+ describe("ToolDispatcher", () => {
14
+ it("should dispatch tool calls and return JSON result", async () => {
15
+ const double = vi.fn(async (...args: unknown[]) => {
16
+ const input = args[0] as Record<string, unknown>;
17
+ return { doubled: (input.n as number) * 2 };
18
+ });
19
+ const fns: ToolFns = { double };
20
+ const dispatcher = new ToolDispatcher(fns);
21
+
22
+ const resJson = await dispatcher.call("double", JSON.stringify({ n: 5 }));
23
+ const data = JSON.parse(resJson);
24
+
25
+ expect(data.result).toEqual({ doubled: 10 });
26
+ expect(double).toHaveBeenCalledWith({ n: 5 });
27
+ });
28
+
29
+ it("should return error for unknown tool", async () => {
30
+ const dispatcher = new ToolDispatcher({});
31
+
32
+ const resJson = await dispatcher.call("nonexistent", "{}");
33
+ const data = JSON.parse(resJson);
34
+
35
+ expect(data.error).toContain("nonexistent");
36
+ });
37
+
38
+ it("should return error when tool function throws", async () => {
39
+ const fns: ToolFns = {
40
+ broken: async () => {
41
+ throw new Error("something broke");
42
+ }
43
+ };
44
+ const dispatcher = new ToolDispatcher(fns);
45
+
46
+ const resJson = await dispatcher.call("broken", "{}");
47
+ const data = JSON.parse(resJson);
48
+
49
+ expect(data.error).toBe("something broke");
50
+ });
51
+
52
+ it("should handle empty args string", async () => {
53
+ const noArgs = vi.fn(async () => "ok");
54
+ const fns: ToolFns = { noArgs };
55
+ const dispatcher = new ToolDispatcher(fns);
56
+
57
+ const resJson = await dispatcher.call("noArgs", "");
58
+ const data = JSON.parse(resJson);
59
+
60
+ expect(data.result).toBe("ok");
61
+ expect(noArgs).toHaveBeenCalledWith({});
62
+ });
63
+ });
64
+
65
+ describe("DynamicWorkerExecutor", () => {
66
+ it("should execute simple code that returns a value", async () => {
67
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
68
+
69
+ const result = await executor.execute("async () => 42", {});
70
+ expect(result.result).toBe(42);
71
+ expect(result.error).toBeUndefined();
72
+ });
73
+
74
+ it("should call tool functions via codemode proxy", async () => {
75
+ const add = vi.fn(async (...args: unknown[]) => {
76
+ const input = args[0] as Record<string, unknown>;
77
+ return (input.a as number) + (input.b as number);
78
+ });
79
+ const fns: ToolFns = { add };
80
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
81
+
82
+ const result = await executor.execute(
83
+ "async () => await codemode.add({ a: 3, b: 4 })",
84
+ fns
85
+ );
86
+
87
+ expect(result.result).toBe(7);
88
+ expect(add).toHaveBeenCalledWith({ a: 3, b: 4 });
89
+ });
90
+
91
+ it("should handle multiple sequential tool calls", async () => {
92
+ const getWeather = vi.fn(async () => ({ temp: 72 }));
93
+ const searchWeb = vi.fn(async (...args: unknown[]) => {
94
+ const input = args[0] as Record<string, unknown>;
95
+ return { results: [`news about ${input.query as string}`] };
96
+ });
97
+ const fns: ToolFns = { getWeather, searchWeb };
98
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
99
+
100
+ const code = `async () => {
101
+ const weather = await codemode.getWeather({});
102
+ const news = await codemode.searchWeb({ query: "temp " + weather.temp });
103
+ return { weather, news };
104
+ }`;
105
+
106
+ const result = await executor.execute(code, fns);
107
+ expect(result.result).toEqual({
108
+ weather: { temp: 72 },
109
+ news: { results: ["news about temp 72"] }
110
+ });
111
+ expect(getWeather).toHaveBeenCalledTimes(1);
112
+ expect(searchWeb).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it("should return error when code throws", async () => {
116
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
117
+
118
+ const result = await executor.execute(
119
+ 'async () => { throw new Error("boom"); }',
120
+ {}
121
+ );
122
+ expect(result.error).toBe("boom");
123
+ });
124
+
125
+ it("should return error when tool function throws", async () => {
126
+ const fail = vi.fn(async () => {
127
+ throw new Error("tool error");
128
+ });
129
+ const fns: ToolFns = { fail };
130
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
131
+
132
+ const result = await executor.execute(
133
+ "async () => await codemode.fail({})",
134
+ fns
135
+ );
136
+ expect(result.error).toBe("tool error");
137
+ });
138
+
139
+ it("should handle concurrent tool calls via Promise.all", async () => {
140
+ const fns: ToolFns = {
141
+ slow: async (...args: unknown[]) => {
142
+ const input = args[0] as Record<string, unknown>;
143
+ return { id: input.id as number };
144
+ }
145
+ };
146
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
147
+
148
+ const code = `async () => {
149
+ const [a, b, c] = await Promise.all([
150
+ codemode.slow({ id: 1 }),
151
+ codemode.slow({ id: 2 }),
152
+ codemode.slow({ id: 3 })
153
+ ]);
154
+ return [a, b, c];
155
+ }`;
156
+
157
+ const result = await executor.execute(code, fns);
158
+ expect(result.result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
159
+ });
160
+
161
+ it("should capture console.log output", async () => {
162
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
163
+
164
+ const result = await executor.execute(
165
+ 'async () => { console.log("hello"); console.warn("careful"); return "done"; }',
166
+ {}
167
+ );
168
+
169
+ expect(result.result).toBe("done");
170
+ expect(result.logs).toContain("hello");
171
+ expect(result.logs).toContain("[warn] careful");
172
+ });
173
+
174
+ it("should handle code containing backticks and template literals", async () => {
175
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
176
+
177
+ const result = await executor.execute(
178
+ 'async () => { return `hello ${"world"}`; }',
179
+ {}
180
+ );
181
+
182
+ expect(result.result).toBe("hello world");
183
+ });
184
+
185
+ it("should block external fetch by default (globalOutbound: null)", async () => {
186
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
187
+
188
+ const result = await executor.execute(
189
+ 'async () => { const r = await fetch("https://example.com"); return r.status; }',
190
+ {}
191
+ );
192
+
193
+ // fetch should fail because globalOutbound defaults to null
194
+ expect(result.error).toBeDefined();
195
+ });
196
+
197
+ it("should preserve closures in tool functions", async () => {
198
+ const secret = "api-key-123";
199
+ const fns: ToolFns = {
200
+ getSecret: async () => ({ key: secret })
201
+ };
202
+ const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
203
+
204
+ const result = await executor.execute(
205
+ "async () => await codemode.getSecret({})",
206
+ fns
207
+ );
208
+ expect(result.result).toEqual({ key: "api-key-123" });
209
+ });
210
+
211
+ it("should include timeout in execution", async () => {
212
+ const executor = new DynamicWorkerExecutor({
213
+ loader: env.LOADER,
214
+ timeout: 100
215
+ });
216
+
217
+ const result = await executor.execute(
218
+ "async () => { await new Promise(r => setTimeout(r, 5000)); return 'done'; }",
219
+ {}
220
+ );
221
+
222
+ expect(result.error).toContain("timed out");
223
+ });
224
+ });