@cloudflare/codemode 0.0.8 → 0.1.0
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.
- package/CHANGELOG.md +16 -0
- package/README.md +174 -247
- package/dist/ai.d.ts +27 -27
- package/dist/ai.js +67 -136
- package/dist/ai.js.map +1 -1
- package/dist/executor-Czw9jKZH.d.ts +96 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/types-B9g5T2nd.js +138 -0
- package/dist/types-B9g5T2nd.js.map +1 -0
- package/e2e/codemode.spec.ts +124 -0
- package/e2e/playwright.config.ts +24 -0
- package/e2e/worker.ts +144 -0
- package/e2e/wrangler.jsonc +14 -0
- package/package.json +15 -4
- package/scripts/build.ts +1 -2
- package/src/ai.ts +1 -247
- package/src/executor.ts +170 -0
- package/src/index.ts +13 -0
- package/src/tests/cloudflare-test.d.ts +5 -0
- package/src/tests/executor.test.ts +224 -0
- package/src/tests/tool.test.ts +454 -0
- package/src/tests/tsconfig.json +10 -0
- package/src/tests/types.test.ts +171 -0
- package/src/tool.ts +131 -0
- package/src/types.ts +202 -0
- package/vitest.config.ts +17 -0
- package/wrangler.jsonc +16 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for createCodeTool — the function that wires tools + executor into
|
|
3
|
+
* a single AI SDK tool.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { createCodeTool } from "../tool";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { ToolDescriptors } from "../types";
|
|
9
|
+
import type { Executor, ExecuteResult } from "../executor";
|
|
10
|
+
|
|
11
|
+
/** A mock executor that records calls and returns configurable results. */
|
|
12
|
+
function createMockExecutor(result: ExecuteResult = { result: "ok" }) {
|
|
13
|
+
const calls: { code: string; fnNames: string[] }[] = [];
|
|
14
|
+
const executor: Executor = {
|
|
15
|
+
execute: vi.fn(async (code, fns) => {
|
|
16
|
+
calls.push({ code, fnNames: Object.keys(fns) });
|
|
17
|
+
return result;
|
|
18
|
+
})
|
|
19
|
+
};
|
|
20
|
+
return { executor, calls };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("createCodeTool", () => {
|
|
24
|
+
const tools: ToolDescriptors = {
|
|
25
|
+
getWeather: {
|
|
26
|
+
description: "Get weather for a location",
|
|
27
|
+
inputSchema: z.object({ location: z.string() }),
|
|
28
|
+
execute: async (_args: unknown) => ({ temp: 72 })
|
|
29
|
+
},
|
|
30
|
+
searchWeb: {
|
|
31
|
+
description: "Search the web",
|
|
32
|
+
inputSchema: z.object({ query: z.string() }),
|
|
33
|
+
execute: async (_args: unknown) => ({ results: [] })
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
it("should return a tool with correct structure", () => {
|
|
38
|
+
const { executor } = createMockExecutor();
|
|
39
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
40
|
+
|
|
41
|
+
expect(codeTool).toBeDefined();
|
|
42
|
+
expect(codeTool.description).toBeDefined();
|
|
43
|
+
expect(codeTool.execute).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should include tool names in the description", () => {
|
|
47
|
+
const { executor } = createMockExecutor();
|
|
48
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
49
|
+
|
|
50
|
+
expect(codeTool.description).toContain("getWeather");
|
|
51
|
+
expect(codeTool.description).toContain("searchWeb");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should include generated types in the description", () => {
|
|
55
|
+
const { executor } = createMockExecutor();
|
|
56
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
57
|
+
|
|
58
|
+
// Should contain the generated TypeScript type names
|
|
59
|
+
expect(codeTool.description).toContain("GetWeatherInput");
|
|
60
|
+
expect(codeTool.description).toContain("SearchWebInput");
|
|
61
|
+
expect(codeTool.description).toContain("declare const codemode");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should support custom description with {{types}} placeholder", () => {
|
|
65
|
+
const { executor } = createMockExecutor();
|
|
66
|
+
const codeTool = createCodeTool({
|
|
67
|
+
tools,
|
|
68
|
+
executor,
|
|
69
|
+
description: "Custom prefix.\n\n{{types}}\n\nCustom suffix."
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(codeTool.description).toContain("Custom prefix.");
|
|
73
|
+
expect(codeTool.description).toContain("Custom suffix.");
|
|
74
|
+
expect(codeTool.description).toContain("getWeather");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should pass code and extracted fns to executor", async () => {
|
|
78
|
+
const { executor, calls } = createMockExecutor();
|
|
79
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
80
|
+
|
|
81
|
+
await codeTool.execute?.(
|
|
82
|
+
{ code: "async () => codemode.getWeather({ location: 'NYC' })" },
|
|
83
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(calls).toHaveLength(1);
|
|
87
|
+
expect(calls[0].code).toBe(
|
|
88
|
+
"async () => codemode.getWeather({ location: 'NYC' })"
|
|
89
|
+
);
|
|
90
|
+
expect(calls[0].fnNames).toContain("getWeather");
|
|
91
|
+
expect(calls[0].fnNames).toContain("searchWeb");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should extract working execute functions from tools", async () => {
|
|
95
|
+
const executeSpy = vi.fn(async (_args: unknown) => ({ temp: 99 }));
|
|
96
|
+
const testTools: ToolDescriptors = {
|
|
97
|
+
myTool: {
|
|
98
|
+
description: "Test",
|
|
99
|
+
inputSchema: z.object({ x: z.number() }),
|
|
100
|
+
execute: executeSpy
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let capturedFns: Record<string, Function> = {};
|
|
105
|
+
const executor: Executor = {
|
|
106
|
+
execute: vi.fn(async (code, fns) => {
|
|
107
|
+
capturedFns = fns;
|
|
108
|
+
// Actually call the fn to verify it works
|
|
109
|
+
const result = await fns.myTool({ x: 42 });
|
|
110
|
+
return { result };
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const codeTool = createCodeTool({ tools: testTools, executor });
|
|
115
|
+
await codeTool.execute?.(
|
|
116
|
+
{ code: "async () => null" },
|
|
117
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(executeSpy).toHaveBeenCalledWith({ x: 42 });
|
|
121
|
+
expect(capturedFns.myTool).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should skip tools without execute functions", async () => {
|
|
125
|
+
const testTools: ToolDescriptors = {
|
|
126
|
+
withExecute: {
|
|
127
|
+
description: "Has execute",
|
|
128
|
+
inputSchema: z.object({}),
|
|
129
|
+
execute: async () => ({})
|
|
130
|
+
},
|
|
131
|
+
withoutExecute: {
|
|
132
|
+
description: "No execute",
|
|
133
|
+
inputSchema: z.object({})
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
let capturedFnNames: string[] = [];
|
|
138
|
+
const executor: Executor = {
|
|
139
|
+
execute: vi.fn(async (_code, fns) => {
|
|
140
|
+
capturedFnNames = Object.keys(fns);
|
|
141
|
+
return { result: null };
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const codeTool = createCodeTool({ tools: testTools, executor });
|
|
146
|
+
await codeTool.execute?.(
|
|
147
|
+
{ code: "async () => null" },
|
|
148
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(capturedFnNames).toContain("withExecute");
|
|
152
|
+
expect(capturedFnNames).not.toContain("withoutExecute");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should exclude tools with needsApproval: true from fns and description", async () => {
|
|
156
|
+
const testTools = {
|
|
157
|
+
safeTool: {
|
|
158
|
+
description: "Safe tool",
|
|
159
|
+
inputSchema: z.object({}),
|
|
160
|
+
execute: async () => ({ ok: true })
|
|
161
|
+
},
|
|
162
|
+
dangerousTool: {
|
|
163
|
+
description: "Dangerous tool",
|
|
164
|
+
inputSchema: z.object({}),
|
|
165
|
+
execute: async () => ({ deleted: true }),
|
|
166
|
+
needsApproval: true
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let capturedFnNames: string[] = [];
|
|
171
|
+
const executor: Executor = {
|
|
172
|
+
execute: vi.fn(async (_code, fns) => {
|
|
173
|
+
capturedFnNames = Object.keys(fns);
|
|
174
|
+
return { result: null };
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const codeTool = createCodeTool({ tools: testTools, executor });
|
|
179
|
+
|
|
180
|
+
expect(codeTool.description).toContain("safeTool");
|
|
181
|
+
expect(codeTool.description).not.toContain("dangerousTool");
|
|
182
|
+
|
|
183
|
+
await codeTool.execute?.(
|
|
184
|
+
{ code: "async () => null" },
|
|
185
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(capturedFnNames).toContain("safeTool");
|
|
189
|
+
expect(capturedFnNames).not.toContain("dangerousTool");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should exclude tools with needsApproval as a function", async () => {
|
|
193
|
+
const testTools = {
|
|
194
|
+
normalTool: {
|
|
195
|
+
description: "Normal",
|
|
196
|
+
inputSchema: z.object({}),
|
|
197
|
+
execute: async () => ({})
|
|
198
|
+
},
|
|
199
|
+
approvalFnTool: {
|
|
200
|
+
description: "Approval fn",
|
|
201
|
+
inputSchema: z.object({}),
|
|
202
|
+
execute: async () => ({}),
|
|
203
|
+
needsApproval: async () => true
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
let capturedFnNames: string[] = [];
|
|
208
|
+
const executor: Executor = {
|
|
209
|
+
execute: vi.fn(async (_code, fns) => {
|
|
210
|
+
capturedFnNames = Object.keys(fns);
|
|
211
|
+
return { result: null };
|
|
212
|
+
})
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const codeTool = createCodeTool({ tools: testTools, executor });
|
|
216
|
+
|
|
217
|
+
expect(codeTool.description).not.toContain("approvalFnTool");
|
|
218
|
+
|
|
219
|
+
await codeTool.execute?.(
|
|
220
|
+
{ code: "async () => null" },
|
|
221
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(capturedFnNames).toContain("normalTool");
|
|
225
|
+
expect(capturedFnNames).not.toContain("approvalFnTool");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should return { code, result } on success", async () => {
|
|
229
|
+
const { executor } = createMockExecutor({ result: { answer: 42 } });
|
|
230
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
231
|
+
|
|
232
|
+
const output = await codeTool.execute?.(
|
|
233
|
+
{ code: "async () => 42" },
|
|
234
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(output).toEqual({
|
|
238
|
+
code: "async () => 42",
|
|
239
|
+
result: { answer: 42 }
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should throw when executor returns error", async () => {
|
|
244
|
+
const { executor } = createMockExecutor({
|
|
245
|
+
result: undefined,
|
|
246
|
+
error: "execution failed"
|
|
247
|
+
});
|
|
248
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
249
|
+
|
|
250
|
+
await expect(
|
|
251
|
+
codeTool.execute?.(
|
|
252
|
+
{ code: "async () => null" },
|
|
253
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
254
|
+
)
|
|
255
|
+
).rejects.toThrow("Code execution failed: execution failed");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should include console output in error message when logs present", async () => {
|
|
259
|
+
const { executor } = createMockExecutor({
|
|
260
|
+
result: undefined,
|
|
261
|
+
error: "runtime error",
|
|
262
|
+
logs: ["debug info", "[error] something went wrong"]
|
|
263
|
+
});
|
|
264
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
265
|
+
|
|
266
|
+
await expect(
|
|
267
|
+
codeTool.execute?.(
|
|
268
|
+
{ code: "async () => null" },
|
|
269
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
270
|
+
)
|
|
271
|
+
).rejects.toThrow("Console output:");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should include logs in successful output", async () => {
|
|
275
|
+
const { executor } = createMockExecutor({
|
|
276
|
+
result: "ok",
|
|
277
|
+
logs: ["log line 1", "log line 2"]
|
|
278
|
+
});
|
|
279
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
280
|
+
|
|
281
|
+
const output = await codeTool.execute?.(
|
|
282
|
+
{ code: "async () => 'ok'" },
|
|
283
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect((output as unknown as Record<string, unknown>)?.logs).toEqual([
|
|
287
|
+
"log line 1",
|
|
288
|
+
"log line 2"
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("code normalization", () => {
|
|
293
|
+
it("should pass arrow functions through unchanged", async () => {
|
|
294
|
+
const { executor, calls } = createMockExecutor();
|
|
295
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
296
|
+
|
|
297
|
+
await codeTool.execute?.(
|
|
298
|
+
{ code: "async () => { return 42; }" },
|
|
299
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(calls[0].code).toBe("async () => { return 42; }");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should splice return into last expression in named-function-then-call pattern", async () => {
|
|
306
|
+
const { executor, calls } = createMockExecutor();
|
|
307
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
308
|
+
|
|
309
|
+
const code = `const fn = async () => { return 42; };\nfn().catch(console.error);`;
|
|
310
|
+
await codeTool.execute?.(
|
|
311
|
+
{ code },
|
|
312
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// AST normalization wraps the last expression in return(...)
|
|
316
|
+
expect(calls[0].code).toContain("async () => {");
|
|
317
|
+
expect(calls[0].code).toContain("return (fn().catch(console.error))");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should not prepend return to declarations on last line", async () => {
|
|
321
|
+
const { executor, calls } = createMockExecutor();
|
|
322
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
323
|
+
|
|
324
|
+
const code = `const x = 1;\nconst y = 2;`;
|
|
325
|
+
await codeTool.execute?.(
|
|
326
|
+
{ code },
|
|
327
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(calls[0].code).toContain("async () => {");
|
|
331
|
+
expect(calls[0].code).not.toContain("return const");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should not prepend return to control flow on last line", async () => {
|
|
335
|
+
const { executor, calls } = createMockExecutor();
|
|
336
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
337
|
+
|
|
338
|
+
const code = `const items = [];\nif (items.length === 0) { return null; }`;
|
|
339
|
+
await codeTool.execute?.(
|
|
340
|
+
{ code },
|
|
341
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(calls[0].code).toContain("async () => {");
|
|
345
|
+
expect(calls[0].code).not.toContain("return if");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should not prepend return when last line already has return", async () => {
|
|
349
|
+
const { executor, calls } = createMockExecutor();
|
|
350
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
351
|
+
|
|
352
|
+
const code = `const r = await codemode.getWeather({ location: "NYC" });\nreturn r;`;
|
|
353
|
+
await codeTool.execute?.(
|
|
354
|
+
{ code },
|
|
355
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
expect(calls[0].code).toContain("async () => {");
|
|
359
|
+
expect(calls[0].code).toContain("return r;");
|
|
360
|
+
expect(calls[0].code).not.toContain("return return");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("should pass parenthesized arrow functions through unchanged", async () => {
|
|
364
|
+
const { executor, calls } = createMockExecutor();
|
|
365
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
366
|
+
|
|
367
|
+
const code = `(async () => { return 42; })`;
|
|
368
|
+
await codeTool.execute?.(
|
|
369
|
+
{ code },
|
|
370
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Parenthesized arrow is still an ArrowFunctionExpression in the AST
|
|
374
|
+
expect(calls[0].code).toBe("(async () => { return 42; })");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should handle template literals with backticks in code", async () => {
|
|
378
|
+
const { executor, calls } = createMockExecutor();
|
|
379
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
380
|
+
|
|
381
|
+
const code = 'async () => { return `hello ${"world"}`; }';
|
|
382
|
+
await codeTool.execute?.(
|
|
383
|
+
{ code },
|
|
384
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(calls[0].code).toBe(code);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("should wrap syntax errors as fallback", async () => {
|
|
391
|
+
const { executor, calls } = createMockExecutor();
|
|
392
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
393
|
+
|
|
394
|
+
const code = `this is not valid javascript @#$`;
|
|
395
|
+
await codeTool.execute?.(
|
|
396
|
+
{ code },
|
|
397
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Falls back to wrapping in async arrow
|
|
401
|
+
expect(calls[0].code).toContain("async () => {");
|
|
402
|
+
expect(calls[0].code).toContain(code);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should return empty async arrow for empty/whitespace input", async () => {
|
|
406
|
+
const { executor, calls } = createMockExecutor();
|
|
407
|
+
const codeTool = createCodeTool({ tools, executor });
|
|
408
|
+
|
|
409
|
+
await codeTool.execute?.(
|
|
410
|
+
{ code: " " },
|
|
411
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
expect(calls[0].code).toBe("async () => {}");
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("should preserve closure state across multiple calls", async () => {
|
|
419
|
+
let counter = 0;
|
|
420
|
+
const testTools: ToolDescriptors = {
|
|
421
|
+
increment: {
|
|
422
|
+
description: "Increment counter",
|
|
423
|
+
inputSchema: z.object({}),
|
|
424
|
+
execute: async () => ({ count: ++counter })
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const executor: Executor = {
|
|
429
|
+
execute: vi.fn(async (_code, fns) => {
|
|
430
|
+
const result = await fns.increment({});
|
|
431
|
+
return { result };
|
|
432
|
+
})
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const codeTool = createCodeTool({ tools: testTools, executor });
|
|
436
|
+
|
|
437
|
+
const r1 = await codeTool.execute?.(
|
|
438
|
+
{ code: "call1" },
|
|
439
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
440
|
+
);
|
|
441
|
+
const r2 = await codeTool.execute?.(
|
|
442
|
+
{ code: "call2" },
|
|
443
|
+
{} as unknown as Parameters<NonNullable<typeof codeTool.execute>>[1]
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect((r1 as unknown as Record<string, unknown>)?.result).toEqual({
|
|
447
|
+
count: 1
|
|
448
|
+
});
|
|
449
|
+
expect((r2 as unknown as Record<string, unknown>)?.result).toEqual({
|
|
450
|
+
count: 2
|
|
451
|
+
});
|
|
452
|
+
expect(counter).toBe(2);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for generateTypes and sanitizeToolName.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { generateTypes, sanitizeToolName } from "../types";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type { ToolDescriptors } from "../types";
|
|
8
|
+
|
|
9
|
+
describe("sanitizeToolName", () => {
|
|
10
|
+
it("should replace hyphens with underscores", () => {
|
|
11
|
+
expect(sanitizeToolName("get-weather")).toBe("get_weather");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should replace dots with underscores", () => {
|
|
15
|
+
expect(sanitizeToolName("api.v2.search")).toBe("api_v2_search");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should replace spaces with underscores", () => {
|
|
19
|
+
expect(sanitizeToolName("my tool")).toBe("my_tool");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should prefix digit-leading names with underscore", () => {
|
|
23
|
+
expect(sanitizeToolName("3drender")).toBe("_3drender");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should append underscore to reserved words", () => {
|
|
27
|
+
expect(sanitizeToolName("class")).toBe("class_");
|
|
28
|
+
expect(sanitizeToolName("return")).toBe("return_");
|
|
29
|
+
expect(sanitizeToolName("delete")).toBe("delete_");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should strip special characters", () => {
|
|
33
|
+
expect(sanitizeToolName("hello@world!")).toBe("helloworld");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should handle empty string", () => {
|
|
37
|
+
expect(sanitizeToolName("")).toBe("_");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should handle string with only special characters", () => {
|
|
41
|
+
// $ is a valid identifier character, so "@#$" → "$"
|
|
42
|
+
expect(sanitizeToolName("@#$")).toBe("$");
|
|
43
|
+
expect(sanitizeToolName("@#!")).toBe("_");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should leave valid identifiers unchanged", () => {
|
|
47
|
+
expect(sanitizeToolName("getWeather")).toBe("getWeather");
|
|
48
|
+
expect(sanitizeToolName("_private")).toBe("_private");
|
|
49
|
+
expect(sanitizeToolName("$jquery")).toBe("$jquery");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("generateTypes", () => {
|
|
54
|
+
it("should generate types for simple tools", () => {
|
|
55
|
+
const tools: ToolDescriptors = {
|
|
56
|
+
getWeather: {
|
|
57
|
+
description: "Get weather for a location",
|
|
58
|
+
inputSchema: z.object({ location: z.string() })
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = generateTypes(tools);
|
|
63
|
+
expect(result).toContain("GetWeatherInput");
|
|
64
|
+
expect(result).toContain("GetWeatherOutput");
|
|
65
|
+
expect(result).toContain("declare const codemode");
|
|
66
|
+
expect(result).toContain("getWeather");
|
|
67
|
+
expect(result).toContain("Get weather for a location");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should generate types for nested schemas", () => {
|
|
71
|
+
const tools: ToolDescriptors = {
|
|
72
|
+
createUser: {
|
|
73
|
+
description: "Create a user",
|
|
74
|
+
inputSchema: z.object({
|
|
75
|
+
name: z.string(),
|
|
76
|
+
address: z.object({
|
|
77
|
+
street: z.string(),
|
|
78
|
+
city: z.string()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = generateTypes(tools);
|
|
85
|
+
expect(result).toContain("CreateUserInput");
|
|
86
|
+
expect(result).toContain("name");
|
|
87
|
+
expect(result).toContain("address");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should handle optional fields", () => {
|
|
91
|
+
const tools: ToolDescriptors = {
|
|
92
|
+
search: {
|
|
93
|
+
description: "Search",
|
|
94
|
+
inputSchema: z.object({
|
|
95
|
+
query: z.string(),
|
|
96
|
+
limit: z.number().optional()
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const result = generateTypes(tools);
|
|
102
|
+
expect(result).toContain("SearchInput");
|
|
103
|
+
expect(result).toContain("query");
|
|
104
|
+
expect(result).toContain("limit");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle enums", () => {
|
|
108
|
+
const tools: ToolDescriptors = {
|
|
109
|
+
sort: {
|
|
110
|
+
description: "Sort items",
|
|
111
|
+
inputSchema: z.object({
|
|
112
|
+
order: z.enum(["asc", "desc"])
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const result = generateTypes(tools);
|
|
118
|
+
expect(result).toContain("SortInput");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should handle arrays", () => {
|
|
122
|
+
const tools: ToolDescriptors = {
|
|
123
|
+
batch: {
|
|
124
|
+
description: "Batch process",
|
|
125
|
+
inputSchema: z.object({
|
|
126
|
+
items: z.array(z.string())
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = generateTypes(tools);
|
|
132
|
+
expect(result).toContain("BatchInput");
|
|
133
|
+
expect(result).toContain("items");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should handle empty tool set", () => {
|
|
137
|
+
const result = generateTypes({});
|
|
138
|
+
expect(result).toContain("declare const codemode");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should include JSDoc param descriptions from z.describe()", () => {
|
|
142
|
+
const tools: ToolDescriptors = {
|
|
143
|
+
search: {
|
|
144
|
+
description: "Search the web",
|
|
145
|
+
inputSchema: z.object({
|
|
146
|
+
query: z.string().describe("The search query"),
|
|
147
|
+
limit: z.number().describe("Max results to return")
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const result = generateTypes(tools);
|
|
153
|
+
expect(result).toContain("@param input.query - The search query");
|
|
154
|
+
expect(result).toContain("@param input.limit - Max results to return");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should sanitize tool names with hyphens", () => {
|
|
158
|
+
const tools: ToolDescriptors = {
|
|
159
|
+
"get-weather": {
|
|
160
|
+
description: "Get weather",
|
|
161
|
+
inputSchema: z.object({ location: z.string() })
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = generateTypes(tools);
|
|
166
|
+
// Tool name in codemode declaration is sanitized
|
|
167
|
+
expect(result).toContain("get_weather");
|
|
168
|
+
// toCamelCase("get_weather") → "GetWeather"
|
|
169
|
+
expect(result).toContain("GetWeatherInput");
|
|
170
|
+
});
|
|
171
|
+
});
|