@cloudflare/codemode 0.1.0 → 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.
- package/CHANGELOG.md +20 -0
- package/README.md +2 -2
- package/dist/ai.js +13 -4
- package/dist/ai.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types-CpkEgXwN.js +392 -0
- package/dist/types-CpkEgXwN.js.map +1 -0
- package/package.json +2 -2
- package/src/tests/schema-conversion.test.ts +1068 -0
- package/src/tests/types.test.ts +275 -0
- package/src/tool.ts +25 -4
- package/src/types.ts +527 -52
- package/dist/types-B9g5T2nd.js +0 -138
- package/dist/types-B9g5T2nd.js.map +0 -1
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for codemode JSON Schema to TypeScript conversion.
|
|
3
|
+
* Focus on our jsonSchemaToTypeString code, not zod-to-ts library.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jsonSchema } from "ai";
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import { generateTypes } from "../types";
|
|
9
|
+
import type { ToolSet } from "ai";
|
|
10
|
+
|
|
11
|
+
// Helper: generateTypes accepts ToolDescriptors | ToolSet but jsonSchema() tools
|
|
12
|
+
// don't satisfy ToolDescriptors (Zod-typed). Cast via ToolSet for test convenience.
|
|
13
|
+
function genTypes(tools: Record<string, unknown>): string {
|
|
14
|
+
return generateTypes(tools as unknown as ToolSet);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("generateTypes with jsonSchema wrapper", () => {
|
|
18
|
+
it("handles simple object schema", () => {
|
|
19
|
+
const tools = {
|
|
20
|
+
getUser: {
|
|
21
|
+
description: "Get a user",
|
|
22
|
+
inputSchema: jsonSchema({
|
|
23
|
+
type: "object" as const,
|
|
24
|
+
properties: {
|
|
25
|
+
id: { type: "string" as const }
|
|
26
|
+
},
|
|
27
|
+
required: ["id"]
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = genTypes(tools);
|
|
33
|
+
|
|
34
|
+
expect(result).toContain("type GetUserInput");
|
|
35
|
+
expect(result).toContain("id: string;");
|
|
36
|
+
expect(result).toContain("type GetUserOutput = unknown");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles nested objects", () => {
|
|
40
|
+
const tools = {
|
|
41
|
+
createOrder: {
|
|
42
|
+
description: "Create an order",
|
|
43
|
+
inputSchema: jsonSchema({
|
|
44
|
+
type: "object" as const,
|
|
45
|
+
properties: {
|
|
46
|
+
user: {
|
|
47
|
+
type: "object" as const,
|
|
48
|
+
properties: {
|
|
49
|
+
name: { type: "string" as const },
|
|
50
|
+
email: { type: "string" as const }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = genTypes(tools);
|
|
59
|
+
|
|
60
|
+
expect(result).toContain("user?:");
|
|
61
|
+
expect(result).toContain("name?: string;");
|
|
62
|
+
expect(result).toContain("email?: string;");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("handles arrays", () => {
|
|
66
|
+
const tools = {
|
|
67
|
+
search: {
|
|
68
|
+
description: "Search",
|
|
69
|
+
inputSchema: jsonSchema({
|
|
70
|
+
type: "object" as const,
|
|
71
|
+
properties: {
|
|
72
|
+
tags: {
|
|
73
|
+
type: "array" as const,
|
|
74
|
+
items: { type: "string" as const }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = genTypes(tools);
|
|
82
|
+
|
|
83
|
+
expect(result).toContain("tags?: string[];");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("handles enums", () => {
|
|
87
|
+
const tools = {
|
|
88
|
+
sort: {
|
|
89
|
+
description: "Sort items",
|
|
90
|
+
inputSchema: jsonSchema({
|
|
91
|
+
type: "object" as const,
|
|
92
|
+
properties: {
|
|
93
|
+
order: {
|
|
94
|
+
type: "string" as const,
|
|
95
|
+
enum: ["asc", "desc"]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = genTypes(tools);
|
|
103
|
+
|
|
104
|
+
expect(result).toContain('"asc" | "desc"');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("handles required vs optional fields", () => {
|
|
108
|
+
const tools = {
|
|
109
|
+
query: {
|
|
110
|
+
description: "Query data",
|
|
111
|
+
inputSchema: jsonSchema({
|
|
112
|
+
type: "object" as const,
|
|
113
|
+
properties: {
|
|
114
|
+
query: { type: "string" as const },
|
|
115
|
+
limit: { type: "number" as const }
|
|
116
|
+
},
|
|
117
|
+
required: ["query"]
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const result = genTypes(tools);
|
|
123
|
+
|
|
124
|
+
expect(result).toContain("query: string;");
|
|
125
|
+
expect(result).toContain("limit?: number;");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("handles descriptions in JSDoc", () => {
|
|
129
|
+
const tools = {
|
|
130
|
+
search: {
|
|
131
|
+
description: "Search the web",
|
|
132
|
+
inputSchema: jsonSchema({
|
|
133
|
+
type: "object" as const,
|
|
134
|
+
properties: {
|
|
135
|
+
query: { type: "string" as const, description: "Search query" },
|
|
136
|
+
limit: { type: "number" as const, description: "Max results" }
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = genTypes(tools);
|
|
143
|
+
|
|
144
|
+
expect(result).toContain("/** Search query */");
|
|
145
|
+
expect(result).toContain("/** Max results */");
|
|
146
|
+
expect(result).toContain("@param input.query - Search query");
|
|
147
|
+
expect(result).toContain("@param input.limit - Max results");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("handles anyOf (union types)", () => {
|
|
151
|
+
const tools = {
|
|
152
|
+
getValue: {
|
|
153
|
+
description: "Get value",
|
|
154
|
+
inputSchema: jsonSchema({
|
|
155
|
+
type: "object" as const,
|
|
156
|
+
properties: {
|
|
157
|
+
value: {
|
|
158
|
+
anyOf: [{ type: "string" as const }, { type: "number" as const }]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = genTypes(tools);
|
|
166
|
+
|
|
167
|
+
expect(result).toContain("string | number");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("handles output schema", () => {
|
|
171
|
+
const tools = {
|
|
172
|
+
getWeather: {
|
|
173
|
+
description: "Get weather",
|
|
174
|
+
inputSchema: jsonSchema({
|
|
175
|
+
type: "object" as const,
|
|
176
|
+
properties: {
|
|
177
|
+
city: { type: "string" as const }
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
outputSchema: jsonSchema({
|
|
181
|
+
type: "object" as const,
|
|
182
|
+
properties: {
|
|
183
|
+
temperature: { type: "number" as const },
|
|
184
|
+
conditions: { type: "string" as const }
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = genTypes(tools);
|
|
191
|
+
|
|
192
|
+
expect(result).toContain("type GetWeatherOutput");
|
|
193
|
+
expect(result).not.toContain("GetWeatherOutput = unknown");
|
|
194
|
+
expect(result).toContain("temperature?: number;");
|
|
195
|
+
expect(result).toContain("conditions?: string;");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("generateTypes with Zod schema", () => {
|
|
200
|
+
it("handles basic Zod object", () => {
|
|
201
|
+
const tools = {
|
|
202
|
+
getUser: {
|
|
203
|
+
description: "Get a user",
|
|
204
|
+
inputSchema: z.object({
|
|
205
|
+
id: z.string()
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const result = genTypes(tools);
|
|
211
|
+
|
|
212
|
+
expect(result).toContain("type GetUserInput");
|
|
213
|
+
expect(result).toContain("id: string");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("handles Zod descriptions", () => {
|
|
217
|
+
const tools = {
|
|
218
|
+
search: {
|
|
219
|
+
description: "Search",
|
|
220
|
+
inputSchema: z.object({
|
|
221
|
+
query: z.string().describe("The search query")
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = genTypes(tools);
|
|
227
|
+
|
|
228
|
+
expect(result).toContain("/** The search query */");
|
|
229
|
+
expect(result).toContain("@param input.query - The search query");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("$ref resolution", () => {
|
|
234
|
+
it("resolves $defs refs", () => {
|
|
235
|
+
const tools = {
|
|
236
|
+
create: {
|
|
237
|
+
description: "Create",
|
|
238
|
+
inputSchema: jsonSchema({
|
|
239
|
+
type: "object" as const,
|
|
240
|
+
properties: {
|
|
241
|
+
address: { $ref: "#/$defs/Address" }
|
|
242
|
+
},
|
|
243
|
+
$defs: {
|
|
244
|
+
Address: {
|
|
245
|
+
type: "object" as const,
|
|
246
|
+
properties: {
|
|
247
|
+
street: { type: "string" as const },
|
|
248
|
+
city: { type: "string" as const }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} as Record<string, unknown>)
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = genTypes(tools);
|
|
257
|
+
|
|
258
|
+
expect(result).toContain("street?: string;");
|
|
259
|
+
expect(result).toContain("city?: string;");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("resolves definitions refs", () => {
|
|
263
|
+
const tools = {
|
|
264
|
+
create: {
|
|
265
|
+
description: "Create",
|
|
266
|
+
inputSchema: jsonSchema({
|
|
267
|
+
type: "object" as const,
|
|
268
|
+
properties: {
|
|
269
|
+
item: { $ref: "#/definitions/Item" }
|
|
270
|
+
},
|
|
271
|
+
definitions: {
|
|
272
|
+
Item: {
|
|
273
|
+
type: "object" as const,
|
|
274
|
+
properties: {
|
|
275
|
+
name: { type: "string" as const }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} as Record<string, unknown>)
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const result = genTypes(tools);
|
|
284
|
+
|
|
285
|
+
expect(result).toContain("name?: string;");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("returns unknown for unresolvable ref", () => {
|
|
289
|
+
const tools = {
|
|
290
|
+
test: {
|
|
291
|
+
description: "Test",
|
|
292
|
+
inputSchema: jsonSchema({
|
|
293
|
+
type: "object" as const,
|
|
294
|
+
properties: {
|
|
295
|
+
val: { $ref: "#/definitions/DoesNotExist" }
|
|
296
|
+
}
|
|
297
|
+
} as Record<string, unknown>)
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = genTypes(tools);
|
|
302
|
+
|
|
303
|
+
expect(result).toContain("val?: unknown;");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns unknown for external URL ref", () => {
|
|
307
|
+
const tools = {
|
|
308
|
+
test: {
|
|
309
|
+
description: "Test",
|
|
310
|
+
inputSchema: jsonSchema({
|
|
311
|
+
type: "object" as const,
|
|
312
|
+
properties: {
|
|
313
|
+
val: { $ref: "https://example.com/schema.json" }
|
|
314
|
+
}
|
|
315
|
+
} as Record<string, unknown>)
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const result = genTypes(tools);
|
|
320
|
+
|
|
321
|
+
expect(result).toContain("val?: unknown;");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("resolves nested ref chains", () => {
|
|
325
|
+
const tools = {
|
|
326
|
+
test: {
|
|
327
|
+
description: "Test",
|
|
328
|
+
inputSchema: jsonSchema({
|
|
329
|
+
type: "object" as const,
|
|
330
|
+
properties: {
|
|
331
|
+
item: { $ref: "#/$defs/Wrapper" }
|
|
332
|
+
},
|
|
333
|
+
$defs: {
|
|
334
|
+
Wrapper: {
|
|
335
|
+
type: "object" as const,
|
|
336
|
+
properties: {
|
|
337
|
+
inner: { $ref: "#/$defs/Inner" }
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
Inner: {
|
|
341
|
+
type: "object" as const,
|
|
342
|
+
properties: {
|
|
343
|
+
value: { type: "number" as const }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} as Record<string, unknown>)
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const result = genTypes(tools);
|
|
352
|
+
|
|
353
|
+
expect(result).toContain("value?: number;");
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("circular schemas", () => {
|
|
358
|
+
it("handles self-referencing $ref without stack overflow", () => {
|
|
359
|
+
const tools = {
|
|
360
|
+
test: {
|
|
361
|
+
description: "Test",
|
|
362
|
+
inputSchema: jsonSchema({
|
|
363
|
+
type: "object" as const,
|
|
364
|
+
properties: {
|
|
365
|
+
child: { $ref: "#" }
|
|
366
|
+
}
|
|
367
|
+
} as Record<string, unknown>)
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Should not throw
|
|
372
|
+
const result = genTypes(tools);
|
|
373
|
+
|
|
374
|
+
expect(result).toContain("type TestInput");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("handles deeply nested schemas hitting depth limit", () => {
|
|
378
|
+
// Build a schema 30 levels deep
|
|
379
|
+
let schema: Record<string, unknown> = { type: "string" };
|
|
380
|
+
for (let i = 0; i < 30; i++) {
|
|
381
|
+
schema = {
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: { nested: schema }
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const tools = {
|
|
388
|
+
deep: {
|
|
389
|
+
description: "Deep",
|
|
390
|
+
inputSchema: jsonSchema(schema)
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Should not throw
|
|
395
|
+
const result = genTypes(tools);
|
|
396
|
+
|
|
397
|
+
expect(result).toContain("type DeepInput");
|
|
398
|
+
// At some point it should hit the depth limit and emit `unknown`
|
|
399
|
+
expect(result).toContain("unknown");
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("boolean property schemas", () => {
|
|
404
|
+
it("maps true schema to unknown and false schema to never", () => {
|
|
405
|
+
const tools = {
|
|
406
|
+
test: {
|
|
407
|
+
description: "Test",
|
|
408
|
+
inputSchema: jsonSchema({
|
|
409
|
+
type: "object" as const,
|
|
410
|
+
properties: {
|
|
411
|
+
anything: true,
|
|
412
|
+
nothing: false
|
|
413
|
+
}
|
|
414
|
+
} as Record<string, unknown>)
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const result = genTypes(tools);
|
|
419
|
+
|
|
420
|
+
expect(result).toContain("anything?: unknown;");
|
|
421
|
+
expect(result).toContain("nothing?: never;");
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("property name safety", () => {
|
|
426
|
+
it("escapes control characters in property names", () => {
|
|
427
|
+
const tools = {
|
|
428
|
+
test: {
|
|
429
|
+
description: "Test",
|
|
430
|
+
inputSchema: jsonSchema({
|
|
431
|
+
type: "object" as const,
|
|
432
|
+
properties: {
|
|
433
|
+
"has\nnewline": { type: "string" as const },
|
|
434
|
+
"has\ttab": { type: "string" as const }
|
|
435
|
+
}
|
|
436
|
+
} as Record<string, unknown>)
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const result = genTypes(tools);
|
|
441
|
+
|
|
442
|
+
expect(result).toContain("\\n");
|
|
443
|
+
expect(result).toContain("\\t");
|
|
444
|
+
expect(result).not.toContain("\n has\n");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("escapes quotes in property names", () => {
|
|
448
|
+
const tools = {
|
|
449
|
+
test: {
|
|
450
|
+
description: "Test",
|
|
451
|
+
inputSchema: jsonSchema({
|
|
452
|
+
type: "object" as const,
|
|
453
|
+
properties: {
|
|
454
|
+
'has"quote': { type: "string" as const }
|
|
455
|
+
}
|
|
456
|
+
} as Record<string, unknown>)
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const result = genTypes(tools);
|
|
461
|
+
|
|
462
|
+
expect(result).toContain('\\"');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("handles empty string property name", () => {
|
|
466
|
+
const tools = {
|
|
467
|
+
test: {
|
|
468
|
+
description: "Test",
|
|
469
|
+
inputSchema: jsonSchema({
|
|
470
|
+
type: "object" as const,
|
|
471
|
+
properties: {
|
|
472
|
+
"": { type: "string" as const }
|
|
473
|
+
}
|
|
474
|
+
} as Record<string, unknown>)
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const result = genTypes(tools);
|
|
479
|
+
|
|
480
|
+
expect(result).toContain('""');
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe("JSDoc safety", () => {
|
|
485
|
+
it("escapes */ in property descriptions", () => {
|
|
486
|
+
const tools = {
|
|
487
|
+
test: {
|
|
488
|
+
description: "Test",
|
|
489
|
+
inputSchema: jsonSchema({
|
|
490
|
+
type: "object" as const,
|
|
491
|
+
properties: {
|
|
492
|
+
field: {
|
|
493
|
+
type: "string" as const,
|
|
494
|
+
description: "Value like */ can break comments"
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} as Record<string, unknown>)
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const result = genTypes(tools);
|
|
502
|
+
|
|
503
|
+
expect(result).toContain("*\\/");
|
|
504
|
+
expect(result).not.toContain("/** Value like */ can");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("escapes */ in tool descriptions", () => {
|
|
508
|
+
const tools = {
|
|
509
|
+
test: {
|
|
510
|
+
description: "A tool with */ in description",
|
|
511
|
+
inputSchema: jsonSchema({
|
|
512
|
+
type: "object" as const,
|
|
513
|
+
properties: { x: { type: "string" as const } }
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const result = genTypes(tools);
|
|
519
|
+
|
|
520
|
+
expect(result).toContain("*\\/");
|
|
521
|
+
expect(result).not.toMatch(/\* A tool with \*\/ in/);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("tuple support", () => {
|
|
526
|
+
it("handles items as array (draft-07 tuples)", () => {
|
|
527
|
+
const tools = {
|
|
528
|
+
test: {
|
|
529
|
+
description: "Test",
|
|
530
|
+
inputSchema: jsonSchema({
|
|
531
|
+
type: "object" as const,
|
|
532
|
+
properties: {
|
|
533
|
+
pair: {
|
|
534
|
+
type: "array" as const,
|
|
535
|
+
items: [{ type: "string" as const }, { type: "number" as const }]
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} as Record<string, unknown>)
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const result = genTypes(tools);
|
|
543
|
+
|
|
544
|
+
expect(result).toContain("[string, number]");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("handles prefixItems (JSON Schema 2020-12)", () => {
|
|
548
|
+
const tools = {
|
|
549
|
+
test: {
|
|
550
|
+
description: "Test",
|
|
551
|
+
inputSchema: jsonSchema({
|
|
552
|
+
type: "object" as const,
|
|
553
|
+
properties: {
|
|
554
|
+
triple: {
|
|
555
|
+
type: "array" as const,
|
|
556
|
+
prefixItems: [
|
|
557
|
+
{ type: "string" as const },
|
|
558
|
+
{ type: "number" as const },
|
|
559
|
+
{ type: "boolean" as const }
|
|
560
|
+
]
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} as Record<string, unknown>)
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const result = genTypes(tools);
|
|
568
|
+
|
|
569
|
+
expect(result).toContain("[string, number, boolean]");
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("nullable support", () => {
|
|
574
|
+
it("applies nullable: true to produce union with null", () => {
|
|
575
|
+
const tools = {
|
|
576
|
+
test: {
|
|
577
|
+
description: "Test",
|
|
578
|
+
inputSchema: jsonSchema({
|
|
579
|
+
type: "object" as const,
|
|
580
|
+
properties: {
|
|
581
|
+
name: { type: "string" as const, nullable: true }
|
|
582
|
+
}
|
|
583
|
+
} as Record<string, unknown>)
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const result = genTypes(tools);
|
|
588
|
+
|
|
589
|
+
expect(result).toContain("string | null");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("does not add null when nullable is not set", () => {
|
|
593
|
+
const tools = {
|
|
594
|
+
test: {
|
|
595
|
+
description: "Test",
|
|
596
|
+
inputSchema: jsonSchema({
|
|
597
|
+
type: "object" as const,
|
|
598
|
+
properties: {
|
|
599
|
+
name: { type: "string" as const }
|
|
600
|
+
}
|
|
601
|
+
} as Record<string, unknown>)
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const result = genTypes(tools);
|
|
606
|
+
|
|
607
|
+
expect(result).toContain("name?: string;");
|
|
608
|
+
expect(result).not.toContain("string | null");
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe("allOf / oneOf", () => {
|
|
613
|
+
it("handles allOf intersection types", () => {
|
|
614
|
+
const tools = {
|
|
615
|
+
test: {
|
|
616
|
+
description: "Test",
|
|
617
|
+
inputSchema: jsonSchema({
|
|
618
|
+
type: "object" as const,
|
|
619
|
+
properties: {
|
|
620
|
+
val: {
|
|
621
|
+
allOf: [
|
|
622
|
+
{
|
|
623
|
+
type: "object" as const,
|
|
624
|
+
properties: { a: { type: "string" as const } }
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
type: "object" as const,
|
|
628
|
+
properties: { b: { type: "number" as const } }
|
|
629
|
+
}
|
|
630
|
+
]
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} as Record<string, unknown>)
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const result = genTypes(tools);
|
|
638
|
+
|
|
639
|
+
expect(result).toContain(" & ");
|
|
640
|
+
expect(result).toContain("a?: string;");
|
|
641
|
+
expect(result).toContain("b?: number;");
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("handles oneOf union types with 3+ members", () => {
|
|
645
|
+
const tools = {
|
|
646
|
+
test: {
|
|
647
|
+
description: "Test",
|
|
648
|
+
inputSchema: jsonSchema({
|
|
649
|
+
type: "object" as const,
|
|
650
|
+
properties: {
|
|
651
|
+
val: {
|
|
652
|
+
oneOf: [
|
|
653
|
+
{ type: "string" as const },
|
|
654
|
+
{ type: "number" as const },
|
|
655
|
+
{ type: "boolean" as const }
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
} as Record<string, unknown>)
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const result = genTypes(tools);
|
|
664
|
+
|
|
665
|
+
expect(result).toContain("string | number | boolean");
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
describe("enum/const escaping", () => {
|
|
670
|
+
it("escapes special chars in enum strings", () => {
|
|
671
|
+
const tools = {
|
|
672
|
+
test: {
|
|
673
|
+
description: "Test",
|
|
674
|
+
inputSchema: jsonSchema({
|
|
675
|
+
type: "object" as const,
|
|
676
|
+
properties: {
|
|
677
|
+
val: {
|
|
678
|
+
type: "string" as const,
|
|
679
|
+
enum: ['say "hello"', "back\\slash"]
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} as Record<string, unknown>)
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const result = genTypes(tools);
|
|
687
|
+
|
|
688
|
+
expect(result).toContain('say \\"hello\\"');
|
|
689
|
+
expect(result).toContain("back\\\\slash");
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("handles null in enum", () => {
|
|
693
|
+
const tools = {
|
|
694
|
+
test: {
|
|
695
|
+
description: "Test",
|
|
696
|
+
inputSchema: jsonSchema({
|
|
697
|
+
type: "object" as const,
|
|
698
|
+
properties: {
|
|
699
|
+
val: { enum: ["a", null, "b"] }
|
|
700
|
+
}
|
|
701
|
+
} as Record<string, unknown>)
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const result = genTypes(tools);
|
|
706
|
+
|
|
707
|
+
expect(result).toContain('"a" | null | "b"');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("escapes special chars in const", () => {
|
|
711
|
+
const tools = {
|
|
712
|
+
test: {
|
|
713
|
+
description: "Test",
|
|
714
|
+
inputSchema: jsonSchema({
|
|
715
|
+
type: "object" as const,
|
|
716
|
+
properties: {
|
|
717
|
+
val: { const: 'line "one"' }
|
|
718
|
+
}
|
|
719
|
+
} as Record<string, unknown>)
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const result = genTypes(tools);
|
|
724
|
+
|
|
725
|
+
expect(result).toContain('line \\"one\\"');
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
describe("type array and integer mapping", () => {
|
|
730
|
+
it('handles type array like ["string", "null"]', () => {
|
|
731
|
+
const tools = {
|
|
732
|
+
test: {
|
|
733
|
+
description: "Test",
|
|
734
|
+
inputSchema: jsonSchema({
|
|
735
|
+
type: "object" as const,
|
|
736
|
+
properties: {
|
|
737
|
+
val: { type: ["string", "null"] }
|
|
738
|
+
}
|
|
739
|
+
} as Record<string, unknown>)
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const result = genTypes(tools);
|
|
744
|
+
|
|
745
|
+
expect(result).toContain("string | null");
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("maps integer to number", () => {
|
|
749
|
+
const tools = {
|
|
750
|
+
test: {
|
|
751
|
+
description: "Test",
|
|
752
|
+
inputSchema: jsonSchema({
|
|
753
|
+
type: "object" as const,
|
|
754
|
+
properties: {
|
|
755
|
+
count: { type: "integer" as const }
|
|
756
|
+
}
|
|
757
|
+
} as Record<string, unknown>)
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const result = genTypes(tools);
|
|
762
|
+
|
|
763
|
+
expect(result).toContain("count?: number;");
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("handles bare array type without items", () => {
|
|
767
|
+
const tools = {
|
|
768
|
+
test: {
|
|
769
|
+
description: "Test",
|
|
770
|
+
inputSchema: jsonSchema({
|
|
771
|
+
type: "object" as const,
|
|
772
|
+
properties: {
|
|
773
|
+
list: { type: "array" as const }
|
|
774
|
+
}
|
|
775
|
+
} as Record<string, unknown>)
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const result = genTypes(tools);
|
|
780
|
+
|
|
781
|
+
expect(result).toContain("list?: unknown[];");
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("handles empty enum as never", () => {
|
|
785
|
+
const tools = {
|
|
786
|
+
test: {
|
|
787
|
+
description: "Test",
|
|
788
|
+
inputSchema: jsonSchema({
|
|
789
|
+
type: "object" as const,
|
|
790
|
+
properties: {
|
|
791
|
+
val: { enum: [] }
|
|
792
|
+
}
|
|
793
|
+
} as Record<string, unknown>)
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const result = genTypes(tools);
|
|
798
|
+
|
|
799
|
+
expect(result).toContain("val?: never;");
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
describe("additionalProperties", () => {
|
|
804
|
+
it("emits index signature for additionalProperties: true", () => {
|
|
805
|
+
const tools = {
|
|
806
|
+
test: {
|
|
807
|
+
description: "Test",
|
|
808
|
+
inputSchema: jsonSchema({
|
|
809
|
+
type: "object" as const,
|
|
810
|
+
properties: {
|
|
811
|
+
name: { type: "string" as const }
|
|
812
|
+
},
|
|
813
|
+
additionalProperties: true
|
|
814
|
+
} as Record<string, unknown>)
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const result = genTypes(tools);
|
|
819
|
+
|
|
820
|
+
expect(result).toContain("name?: string;");
|
|
821
|
+
expect(result).toContain("[key: string]: unknown;");
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it("emits typed index signature for typed additionalProperties", () => {
|
|
825
|
+
const tools = {
|
|
826
|
+
test: {
|
|
827
|
+
description: "Test",
|
|
828
|
+
inputSchema: jsonSchema({
|
|
829
|
+
type: "object" as const,
|
|
830
|
+
additionalProperties: { type: "string" as const }
|
|
831
|
+
} as Record<string, unknown>)
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const result = genTypes(tools);
|
|
836
|
+
|
|
837
|
+
expect(result).toContain("[key: string]: string;");
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
describe("additionalProperties: false", () => {
|
|
842
|
+
it("returns empty object type when no properties and additionalProperties is false", () => {
|
|
843
|
+
const tools = {
|
|
844
|
+
test: {
|
|
845
|
+
description: "Test",
|
|
846
|
+
inputSchema: jsonSchema({
|
|
847
|
+
type: "object" as const,
|
|
848
|
+
additionalProperties: false
|
|
849
|
+
} as Record<string, unknown>)
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const result = genTypes(tools);
|
|
854
|
+
|
|
855
|
+
expect(result).toContain("type TestInput = {}");
|
|
856
|
+
expect(result).not.toContain("Record<string, unknown>");
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it("returns Record<string, unknown> when no properties and no additionalProperties constraint", () => {
|
|
860
|
+
const tools = {
|
|
861
|
+
test: {
|
|
862
|
+
description: "Test",
|
|
863
|
+
inputSchema: jsonSchema({
|
|
864
|
+
type: "object" as const
|
|
865
|
+
})
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const result = genTypes(tools);
|
|
870
|
+
|
|
871
|
+
expect(result).toContain("Record<string, unknown>");
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
describe("enum/const object values", () => {
|
|
876
|
+
it("serializes object enum values with JSON.stringify", () => {
|
|
877
|
+
const tools = {
|
|
878
|
+
test: {
|
|
879
|
+
description: "Test",
|
|
880
|
+
inputSchema: jsonSchema({
|
|
881
|
+
type: "object" as const,
|
|
882
|
+
properties: {
|
|
883
|
+
val: { enum: [{ key: "value" }, "plain"] }
|
|
884
|
+
}
|
|
885
|
+
} as Record<string, unknown>)
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const result = genTypes(tools);
|
|
890
|
+
|
|
891
|
+
expect(result).toContain('{"key":"value"}');
|
|
892
|
+
expect(result).not.toContain("[object Object]");
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("serializes array enum values with JSON.stringify", () => {
|
|
896
|
+
const tools = {
|
|
897
|
+
test: {
|
|
898
|
+
description: "Test",
|
|
899
|
+
inputSchema: jsonSchema({
|
|
900
|
+
type: "object" as const,
|
|
901
|
+
properties: {
|
|
902
|
+
val: { enum: [[1, 2, 3], "plain"] }
|
|
903
|
+
}
|
|
904
|
+
} as Record<string, unknown>)
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const result = genTypes(tools);
|
|
909
|
+
|
|
910
|
+
expect(result).toContain("[1,2,3]");
|
|
911
|
+
expect(result).not.toContain("[object Object]");
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it("serializes object const values with JSON.stringify", () => {
|
|
915
|
+
const tools = {
|
|
916
|
+
test: {
|
|
917
|
+
description: "Test",
|
|
918
|
+
inputSchema: jsonSchema({
|
|
919
|
+
type: "object" as const,
|
|
920
|
+
properties: {
|
|
921
|
+
val: { const: { nested: true } }
|
|
922
|
+
}
|
|
923
|
+
} as Record<string, unknown>)
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const result = genTypes(tools);
|
|
928
|
+
|
|
929
|
+
expect(result).toContain('{"nested":true}');
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
describe("multi-line JSDoc format", () => {
|
|
934
|
+
it("uses multi-line JSDoc when both description and format are present", () => {
|
|
935
|
+
const tools = {
|
|
936
|
+
test: {
|
|
937
|
+
description: "Test",
|
|
938
|
+
inputSchema: jsonSchema({
|
|
939
|
+
type: "object" as const,
|
|
940
|
+
properties: {
|
|
941
|
+
email: {
|
|
942
|
+
type: "string" as const,
|
|
943
|
+
description: "User email address",
|
|
944
|
+
format: "email"
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} as Record<string, unknown>)
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
const result = genTypes(tools);
|
|
952
|
+
|
|
953
|
+
expect(result).toContain("* User email address");
|
|
954
|
+
expect(result).toContain("* @format email");
|
|
955
|
+
// Should be multi-line, not single-line
|
|
956
|
+
expect(result).not.toContain("/** User email address @format email */");
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it("uses single-line JSDoc when only format is present", () => {
|
|
960
|
+
const tools = {
|
|
961
|
+
test: {
|
|
962
|
+
description: "Test",
|
|
963
|
+
inputSchema: jsonSchema({
|
|
964
|
+
type: "object" as const,
|
|
965
|
+
properties: {
|
|
966
|
+
id: {
|
|
967
|
+
type: "string" as const,
|
|
968
|
+
format: "uuid"
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} as Record<string, unknown>)
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const result = genTypes(tools);
|
|
976
|
+
|
|
977
|
+
expect(result).toContain("/** @format uuid */");
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
describe("newline normalization in descriptions", () => {
|
|
982
|
+
it("normalizes newlines in property descriptions", () => {
|
|
983
|
+
const tools = {
|
|
984
|
+
test: {
|
|
985
|
+
description: "Test",
|
|
986
|
+
inputSchema: jsonSchema({
|
|
987
|
+
type: "object" as const,
|
|
988
|
+
properties: {
|
|
989
|
+
field: {
|
|
990
|
+
type: "string" as const,
|
|
991
|
+
description: "Line one\nLine two\r\nLine three"
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} as Record<string, unknown>)
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const result = genTypes(tools);
|
|
999
|
+
|
|
1000
|
+
expect(result).toContain("/** Line one Line two Line three */");
|
|
1001
|
+
expect(result).not.toContain("Line one\n");
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it("normalizes newlines in tool descriptions", () => {
|
|
1005
|
+
const tools = {
|
|
1006
|
+
test: {
|
|
1007
|
+
description: "Tool that does\nmultiple things\r\non multiple lines",
|
|
1008
|
+
inputSchema: jsonSchema({
|
|
1009
|
+
type: "object" as const,
|
|
1010
|
+
properties: { x: { type: "string" as const } }
|
|
1011
|
+
})
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const result = genTypes(tools);
|
|
1016
|
+
|
|
1017
|
+
expect(result).toContain(
|
|
1018
|
+
"Tool that does multiple things on multiple lines"
|
|
1019
|
+
);
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
describe("generateTypes codemode declaration", () => {
|
|
1024
|
+
it("generates proper codemode declaration", () => {
|
|
1025
|
+
const tools = {
|
|
1026
|
+
tool1: {
|
|
1027
|
+
description: "First tool",
|
|
1028
|
+
inputSchema: jsonSchema({
|
|
1029
|
+
type: "object" as const,
|
|
1030
|
+
properties: { a: { type: "string" as const } }
|
|
1031
|
+
})
|
|
1032
|
+
},
|
|
1033
|
+
tool2: {
|
|
1034
|
+
description: "Second tool",
|
|
1035
|
+
inputSchema: jsonSchema({
|
|
1036
|
+
type: "object" as const,
|
|
1037
|
+
properties: { b: { type: "number" as const } }
|
|
1038
|
+
})
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const result = genTypes(tools);
|
|
1043
|
+
|
|
1044
|
+
expect(result).toContain("declare const codemode: {");
|
|
1045
|
+
expect(result).toContain(
|
|
1046
|
+
"tool1: (input: Tool1Input) => Promise<Tool1Output>;"
|
|
1047
|
+
);
|
|
1048
|
+
expect(result).toContain(
|
|
1049
|
+
"tool2: (input: Tool2Input) => Promise<Tool2Output>;"
|
|
1050
|
+
);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it("sanitizes tool names in declaration", () => {
|
|
1054
|
+
const tools = {
|
|
1055
|
+
"get-user": {
|
|
1056
|
+
description: "Get user",
|
|
1057
|
+
inputSchema: jsonSchema({
|
|
1058
|
+
type: "object" as const,
|
|
1059
|
+
properties: { id: { type: "string" as const } }
|
|
1060
|
+
})
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
const result = genTypes(tools);
|
|
1065
|
+
|
|
1066
|
+
expect(result).toContain("get_user: (input: GetUserInput)");
|
|
1067
|
+
});
|
|
1068
|
+
});
|