@decocms/bindings 1.4.3 → 1.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/bindings",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -20,25 +20,6 @@ type Tool = {
20
20
 
21
21
  const toolsMap = new Map<string, Promise<Array<Tool>>>();
22
22
 
23
- // Memoize JSON-schema → Zod conversion by schema content. Zod v4 schemas carry
24
- // their methods per-instance, so a freshly-converted schema is ~18 Function/
25
- // AsyncFunction nodes; `mapTool` runs on EVERY `asCallableTools()`/`asTool()`
26
- // call, so without this an MCP server's whole tool set was re-minted into a new
27
- // Zod graph per call and retained by whatever held the result — a dominant
28
- // heap-leak vector server-side (runtime/workflows/sandbox hit this path). The
29
- // conversion is pure and Zod schemas are immutable after build, so sharing one
30
- // instance per distinct schema is safe. Distinct schema contents are bounded by
31
- // the tool catalogue, so no eviction is needed (mirrors sharedJsonSchemaValidator).
32
- const zodSchemaCache = new Map<string, unknown>();
33
- const cachedConvertJsonSchemaToZod = (schema: any) => {
34
- const key = JSON.stringify(schema);
35
- const hit = zodSchemaCache.get(key);
36
- if (hit !== undefined) return hit;
37
- const zod = convertJsonSchemaToZod(schema);
38
- zodSchemaCache.set(key, zod);
39
- return zod;
40
- };
41
-
42
23
  const mapTool = (
43
24
  tool: Tool,
44
25
  callToolFn: (input: any, toolName?: string) => Promise<any>,
@@ -47,10 +28,10 @@ const mapTool = (
47
28
  ...tool,
48
29
  id: tool.name,
49
30
  inputSchema: tool.inputSchema
50
- ? cachedConvertJsonSchemaToZod(tool.inputSchema)
31
+ ? convertJsonSchemaToZod(tool.inputSchema)
51
32
  : undefined,
52
33
  outputSchema: tool.outputSchema
53
- ? cachedConvertJsonSchemaToZod(tool.outputSchema)
34
+ ? convertJsonSchemaToZod(tool.outputSchema)
54
35
  : undefined,
55
36
  execute: (input: any) => {
56
37
  return callToolFn(input.context, tool.name);
@@ -1,943 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { z } from "zod";
3
- import {
4
- createBindingChecker,
5
- type Binder,
6
- type ToolBinder,
7
- } from "../src/index";
8
-
9
- // Skipping tests for now
10
- describe.skip("@decocms/bindings", () => {
11
- describe("ToolBinder type", () => {
12
- it("should define a valid tool binder", () => {
13
- const toolBinder: ToolBinder = {
14
- name: "TEST_TOOL",
15
- inputSchema: z.object({ id: z.string() }),
16
- outputSchema: z.object({ success: z.boolean() }),
17
- };
18
-
19
- expect(toolBinder.name).toBe("TEST_TOOL");
20
- expect(toolBinder.inputSchema).toBeDefined();
21
- expect(toolBinder.outputSchema).toBeDefined();
22
- });
23
-
24
- it("should support optional tools", () => {
25
- const optionalTool: ToolBinder = {
26
- name: "OPTIONAL_TOOL",
27
- inputSchema: z.object({}),
28
- opt: true,
29
- };
30
-
31
- expect(optionalTool.opt).toBe(true);
32
- });
33
-
34
- it("should support RegExp names", () => {
35
- const regexTool: ToolBinder<RegExp> = {
36
- name: /^TEST_\w+$/ as RegExp,
37
- inputSchema: z.object({}),
38
- };
39
-
40
- expect(regexTool.name).toBeInstanceOf(RegExp);
41
- });
42
- });
43
-
44
- describe("Binder type", () => {
45
- it("should define a valid binding with multiple tools", () => {
46
- const binding = [
47
- {
48
- name: "TOOL_ONE" as const,
49
- inputSchema: z.object({ data: z.string() }),
50
- outputSchema: z.object({ result: z.boolean() }),
51
- },
52
- {
53
- name: "TOOL_TWO" as const,
54
- inputSchema: z.object({ id: z.number() }),
55
- outputSchema: z.object({ value: z.string() }),
56
- },
57
- ] as const satisfies Binder;
58
-
59
- expect(binding).toHaveLength(2);
60
- expect(binding[0].name).toBe("TOOL_ONE");
61
- expect(binding[1].name).toBe("TOOL_TWO");
62
- });
63
- });
64
-
65
- describe("createBindingChecker", () => {
66
- const SAMPLE_BINDING = [
67
- {
68
- name: "REQUIRED_TOOL" as const,
69
- inputSchema: z.object({ id: z.string() }),
70
- outputSchema: z.object({ success: z.boolean() }),
71
- },
72
- {
73
- name: "ANOTHER_REQUIRED" as const,
74
- inputSchema: z.object({ value: z.number() }),
75
- outputSchema: z.object({ result: z.string() }),
76
- },
77
- {
78
- name: "OPTIONAL_TOOL" as const,
79
- inputSchema: z.object({}),
80
- opt: true,
81
- },
82
- ] as const satisfies Binder;
83
-
84
- it("should create a binding checker", () => {
85
- const checker = createBindingChecker(SAMPLE_BINDING);
86
-
87
- expect(checker).toBeDefined();
88
- expect(checker.isImplementedBy).toBeInstanceOf(Function);
89
- });
90
-
91
- it("should return true when all required tools are present with compatible schemas", () => {
92
- const checker = createBindingChecker(SAMPLE_BINDING);
93
-
94
- const tools = [
95
- {
96
- name: "REQUIRED_TOOL",
97
- inputSchema: z.object({ id: z.string() }),
98
- outputSchema: z.object({ success: z.boolean() }),
99
- },
100
- {
101
- name: "ANOTHER_REQUIRED",
102
- inputSchema: z.object({ value: z.number() }),
103
- outputSchema: z.object({ result: z.string() }),
104
- },
105
- {
106
- name: "OPTIONAL_TOOL",
107
- inputSchema: z.object({}),
108
- },
109
- ];
110
-
111
- expect(checker.isImplementedBy(tools)).toBe(true);
112
- });
113
-
114
- it("should return true when optional tools are missing", () => {
115
- const checker = createBindingChecker(SAMPLE_BINDING);
116
-
117
- const tools = [
118
- {
119
- name: "REQUIRED_TOOL",
120
- inputSchema: z.object({ id: z.string() }),
121
- outputSchema: z.object({ success: z.boolean() }),
122
- },
123
- {
124
- name: "ANOTHER_REQUIRED",
125
- inputSchema: z.object({ value: z.number() }),
126
- outputSchema: z.object({ result: z.string() }),
127
- },
128
- // OPTIONAL_TOOL is missing, but that's OK
129
- ];
130
-
131
- expect(checker.isImplementedBy(tools)).toBe(true);
132
- });
133
-
134
- it("should return false when required tools are missing", () => {
135
- const checker = createBindingChecker(SAMPLE_BINDING);
136
-
137
- const tools = [
138
- {
139
- name: "REQUIRED_TOOL",
140
- inputSchema: z.object({ id: z.string() }),
141
- outputSchema: z.object({ success: z.boolean() }),
142
- },
143
- // ANOTHER_REQUIRED is missing
144
- ];
145
-
146
- expect(checker.isImplementedBy(tools)).toBe(false);
147
- });
148
-
149
- it("should work with extra tools present", () => {
150
- const checker = createBindingChecker(SAMPLE_BINDING);
151
-
152
- const tools = [
153
- {
154
- name: "REQUIRED_TOOL",
155
- inputSchema: z.object({ id: z.string() }),
156
- outputSchema: z.object({ success: z.boolean() }),
157
- },
158
- {
159
- name: "ANOTHER_REQUIRED",
160
- inputSchema: z.object({ value: z.number() }),
161
- outputSchema: z.object({ result: z.string() }),
162
- },
163
- {
164
- name: "EXTRA_TOOL_1",
165
- inputSchema: z.object({}),
166
- },
167
- {
168
- name: "EXTRA_TOOL_2",
169
- inputSchema: z.object({}),
170
- },
171
- ];
172
-
173
- expect(checker.isImplementedBy(tools)).toBe(true);
174
- });
175
-
176
- it("should return false when tool input schema is incompatible (wrong type)", () => {
177
- const checker = createBindingChecker(SAMPLE_BINDING);
178
-
179
- const tools = [
180
- {
181
- name: "REQUIRED_TOOL",
182
- // Tool expects number but binder requires string
183
- inputSchema: z.object({ id: z.number() }),
184
- outputSchema: z.object({ success: z.boolean() }),
185
- },
186
- {
187
- name: "ANOTHER_REQUIRED",
188
- inputSchema: z.object({ value: z.number() }),
189
- outputSchema: z.object({ result: z.string() }),
190
- },
191
- ];
192
-
193
- expect(checker.isImplementedBy(tools)).toBe(false);
194
- });
195
-
196
- it("should return false when tool input schema is missing required fields", () => {
197
- const checker = createBindingChecker(SAMPLE_BINDING);
198
-
199
- const tools = [
200
- {
201
- name: "REQUIRED_TOOL",
202
- // Tool missing required 'id' field
203
- inputSchema: z.object({}),
204
- outputSchema: z.object({ success: z.boolean() }),
205
- },
206
- {
207
- name: "ANOTHER_REQUIRED",
208
- inputSchema: z.object({ value: z.number() }),
209
- outputSchema: z.object({ result: z.string() }),
210
- },
211
- ];
212
-
213
- // json-schema-diff should detect missing required properties
214
- const result = checker.isImplementedBy(tools);
215
- // Note: json-schema-diff may or may not detect missing required fields
216
- // depending on how it handles the schema conversion
217
- expect(typeof result).toBe("boolean");
218
- });
219
-
220
- it("should return false when tool input schema has required field as optional", () => {
221
- const checker = createBindingChecker(SAMPLE_BINDING);
222
-
223
- const tools = [
224
- {
225
- name: "REQUIRED_TOOL",
226
- // Binder requires 'id' but tool makes it optional
227
- inputSchema: z.object({ id: z.string().optional() }),
228
- outputSchema: z.object({ success: z.boolean() }),
229
- },
230
- {
231
- name: "ANOTHER_REQUIRED",
232
- inputSchema: z.object({ value: z.number() }),
233
- outputSchema: z.object({ result: z.string() }),
234
- },
235
- ];
236
-
237
- // json-schema-diff should detect that required field became optional
238
- const result = checker.isImplementedBy(tools);
239
- // Note: json-schema-diff may not always detect required->optional changes
240
- expect(typeof result).toBe("boolean");
241
- });
242
-
243
- it("should return false when tool output schema is incompatible (wrong type)", () => {
244
- const checker = createBindingChecker(SAMPLE_BINDING);
245
-
246
- const tools = [
247
- {
248
- name: "REQUIRED_TOOL",
249
- inputSchema: z.object({ id: z.string() }),
250
- // Tool outputs string but binder expects boolean
251
- outputSchema: z.object({ success: z.string() }),
252
- },
253
- {
254
- name: "ANOTHER_REQUIRED",
255
- inputSchema: z.object({ value: z.number() }),
256
- outputSchema: z.object({ result: z.string() }),
257
- },
258
- ];
259
-
260
- expect(checker.isImplementedBy(tools)).toBe(false);
261
- });
262
-
263
- it("should return false when tool output schema is missing required fields", () => {
264
- const checker = createBindingChecker(SAMPLE_BINDING);
265
-
266
- const tools = [
267
- {
268
- name: "REQUIRED_TOOL",
269
- inputSchema: z.object({ id: z.string() }),
270
- // Tool missing required 'success' field
271
- outputSchema: z.object({}),
272
- },
273
- {
274
- name: "ANOTHER_REQUIRED",
275
- inputSchema: z.object({ value: z.number() }),
276
- outputSchema: z.object({ result: z.string() }),
277
- },
278
- ];
279
-
280
- // json-schema-diff should detect missing required output properties
281
- const result = checker.isImplementedBy(tools);
282
- // Note: json-schema-diff may or may not detect missing required fields
283
- expect(typeof result).toBe("boolean");
284
- });
285
-
286
- it("should return false when tool has no input schema but binder requires one", () => {
287
- const checker = createBindingChecker(SAMPLE_BINDING);
288
-
289
- const tools = [
290
- {
291
- name: "REQUIRED_TOOL",
292
- // Tool has no input schema
293
- outputSchema: z.object({ success: z.boolean() }),
294
- },
295
- {
296
- name: "ANOTHER_REQUIRED",
297
- inputSchema: z.object({ value: z.number() }),
298
- outputSchema: z.object({ result: z.string() }),
299
- },
300
- ];
301
-
302
- expect(checker.isImplementedBy(tools)).toBe(false);
303
- });
304
-
305
- it("should return false when tool has no output schema but binder requires one", () => {
306
- const checker = createBindingChecker(SAMPLE_BINDING);
307
-
308
- const tools = [
309
- {
310
- name: "REQUIRED_TOOL",
311
- inputSchema: z.object({ id: z.string() }),
312
- // Tool has no output schema
313
- },
314
- {
315
- name: "ANOTHER_REQUIRED",
316
- inputSchema: z.object({ value: z.number() }),
317
- outputSchema: z.object({ result: z.string() }),
318
- },
319
- ];
320
-
321
- expect(checker.isImplementedBy(tools)).toBe(false);
322
- });
323
-
324
- it("should allow tool to accept additional input fields", () => {
325
- const checker = createBindingChecker(SAMPLE_BINDING);
326
-
327
- const tools = [
328
- {
329
- name: "REQUIRED_TOOL",
330
- // Tool accepts id (required) + optional extra field
331
- inputSchema: z.object({
332
- id: z.string(),
333
- extra: z.string().optional(),
334
- }),
335
- outputSchema: z.object({ success: z.boolean() }),
336
- },
337
- {
338
- name: "ANOTHER_REQUIRED",
339
- inputSchema: z.object({ value: z.number() }),
340
- outputSchema: z.object({ result: z.string() }),
341
- },
342
- ];
343
-
344
- // Tools should be able to accept additional fields (more permissive)
345
- // Note: json-schema-diff might be strict about additionalProperties
346
- const result = checker.isImplementedBy(tools);
347
- // The result depends on json-schema-diff's handling of additionalProperties
348
- expect(typeof result).toBe("boolean");
349
- });
350
-
351
- it("should allow tool to provide additional output fields", () => {
352
- const checker = createBindingChecker(SAMPLE_BINDING);
353
-
354
- const tools = [
355
- {
356
- name: "REQUIRED_TOOL",
357
- inputSchema: z.object({ id: z.string() }),
358
- // Tool provides success (required) + extra field
359
- outputSchema: z.object({
360
- success: z.boolean(),
361
- timestamp: z.number(),
362
- }),
363
- },
364
- {
365
- name: "ANOTHER_REQUIRED",
366
- inputSchema: z.object({ value: z.number() }),
367
- outputSchema: z.object({ result: z.string() }),
368
- },
369
- ];
370
-
371
- // Tools should be able to provide additional output fields (more permissive)
372
- // Note: json-schema-diff might be strict about additionalProperties
373
- const result = checker.isImplementedBy(tools);
374
- // The result depends on json-schema-diff's handling of additionalProperties
375
- expect(typeof result).toBe("boolean");
376
- });
377
- });
378
-
379
- describe("Complex schema validation", () => {
380
- const COMPLEX_BINDING = [
381
- {
382
- name: "COMPLEX_TOOL" as const,
383
- inputSchema: z.object({
384
- user: z.object({
385
- id: z.string(),
386
- email: z.string().email(),
387
- profile: z.object({
388
- name: z.string(),
389
- age: z.number().optional(),
390
- }),
391
- }),
392
- tags: z.array(z.string()),
393
- metadata: z.record(z.string(), z.any()),
394
- }),
395
- outputSchema: z.object({
396
- result: z.object({
397
- id: z.string(),
398
- status: z.enum(["success", "error"]),
399
- data: z.array(z.object({ value: z.number() })),
400
- }),
401
- timestamp: z.string().datetime(),
402
- }),
403
- },
404
- ] as const satisfies Binder;
405
-
406
- it("should pass when tool accepts all nested required fields", () => {
407
- const checker = createBindingChecker(COMPLEX_BINDING);
408
-
409
- const tools = [
410
- {
411
- name: "COMPLEX_TOOL",
412
- inputSchema: z.object({
413
- user: z.object({
414
- id: z.string(),
415
- email: z.string().email(),
416
- profile: z.object({
417
- name: z.string(),
418
- age: z.number().optional(),
419
- }),
420
- }),
421
- tags: z.array(z.string()),
422
- metadata: z.record(z.string(), z.any()),
423
- }),
424
- outputSchema: z.object({
425
- result: z.object({
426
- id: z.string(),
427
- status: z.enum(["success", "error"]),
428
- data: z.array(z.object({ value: z.number() })),
429
- }),
430
- timestamp: z.string().datetime(),
431
- }),
432
- },
433
- ];
434
-
435
- expect(checker.isImplementedBy(tools)).toBe(true);
436
- });
437
-
438
- it("should pass when tool accepts additional nested fields", () => {
439
- const checker = createBindingChecker(COMPLEX_BINDING);
440
-
441
- const tools = [
442
- {
443
- name: "COMPLEX_TOOL",
444
- inputSchema: z.object({
445
- user: z.object({
446
- id: z.string(),
447
- email: z.string().email(),
448
- profile: z.object({
449
- name: z.string(),
450
- age: z.number().optional(),
451
- avatar: z.string().optional(), // Extra field
452
- }),
453
- role: z.string().optional(), // Extra field
454
- }),
455
- tags: z.array(z.string()),
456
- metadata: z.record(z.string(), z.any()),
457
- extra: z.string().optional(), // Extra top-level field
458
- }),
459
- outputSchema: z.object({
460
- result: z.object({
461
- id: z.string(),
462
- status: z.enum(["success", "error"]),
463
- data: z.array(z.object({ value: z.number() })),
464
- }),
465
- timestamp: z.string().datetime(),
466
- extra: z.number().optional(), // Extra output field
467
- }),
468
- },
469
- ];
470
-
471
- // Tools should be able to accept/provide additional fields
472
- // Note: json-schema-diff might be strict about additionalProperties
473
- const result = checker.isImplementedBy(tools);
474
- // The result depends on json-schema-diff's handling of additionalProperties
475
- expect(typeof result).toBe("boolean");
476
- });
477
-
478
- it("should fail when tool is missing nested required fields", () => {
479
- const checker = createBindingChecker(COMPLEX_BINDING);
480
-
481
- const tools = [
482
- {
483
- name: "COMPLEX_TOOL",
484
- inputSchema: z.object({
485
- user: z.object({
486
- id: z.string(),
487
- // Missing required 'email' field
488
- profile: z.object({
489
- name: z.string(),
490
- age: z.number().optional(),
491
- }),
492
- }),
493
- tags: z.array(z.string()),
494
- metadata: z.record(z.string(), z.any()),
495
- }),
496
- outputSchema: z.object({
497
- result: z.object({
498
- id: z.string(),
499
- status: z.enum(["success", "error"]),
500
- data: z.array(z.object({ value: z.number() })),
501
- }),
502
- timestamp: z.string().datetime(),
503
- }),
504
- },
505
- ];
506
-
507
- expect(checker.isImplementedBy(tools)).toBe(false);
508
- });
509
-
510
- it("should fail when tool has wrong nested field type", () => {
511
- const checker = createBindingChecker(COMPLEX_BINDING);
512
-
513
- const tools = [
514
- {
515
- name: "COMPLEX_TOOL",
516
- inputSchema: z.object({
517
- user: z.object({
518
- id: z.string(),
519
- email: z.string().email(),
520
- profile: z.object({
521
- name: z.number(), // Wrong type: should be string
522
- age: z.number().optional(),
523
- }),
524
- }),
525
- tags: z.array(z.string()),
526
- metadata: z.record(z.string(), z.any()),
527
- }),
528
- outputSchema: z.object({
529
- result: z.object({
530
- id: z.string(),
531
- status: z.enum(["success", "error"]),
532
- data: z.array(z.object({ value: z.number() })),
533
- }),
534
- timestamp: z.string().datetime(),
535
- }),
536
- },
537
- ];
538
-
539
- expect(checker.isImplementedBy(tools)).toBe(false);
540
- });
541
-
542
- it("should fail when tool output is missing nested required fields", () => {
543
- const checker = createBindingChecker(COMPLEX_BINDING);
544
-
545
- const tools = [
546
- {
547
- name: "COMPLEX_TOOL",
548
- inputSchema: z.object({
549
- user: z.object({
550
- id: z.string(),
551
- email: z.string().email(),
552
- profile: z.object({
553
- name: z.string(),
554
- age: z.number().optional(),
555
- }),
556
- }),
557
- tags: z.array(z.string()),
558
- metadata: z.record(z.string(), z.any()),
559
- }),
560
- outputSchema: z.object({
561
- result: z.object({
562
- id: z.string(),
563
- status: z.enum(["success", "error"]),
564
- // Missing required 'data' field
565
- }),
566
- timestamp: z.string().datetime(),
567
- }),
568
- },
569
- ];
570
-
571
- expect(checker.isImplementedBy(tools)).toBe(false);
572
- });
573
-
574
- it("should fail when tool output has wrong nested field type", () => {
575
- const checker = createBindingChecker(COMPLEX_BINDING);
576
-
577
- const tools = [
578
- {
579
- name: "COMPLEX_TOOL",
580
- inputSchema: z.object({
581
- user: z.object({
582
- id: z.string(),
583
- email: z.string().email(),
584
- profile: z.object({
585
- name: z.string(),
586
- age: z.number().optional(),
587
- }),
588
- }),
589
- tags: z.array(z.string()),
590
- metadata: z.record(z.string(), z.any()),
591
- }),
592
- outputSchema: z.object({
593
- result: z.object({
594
- id: z.string(),
595
- status: z.string(), // Wrong type: should be enum
596
- data: z.array(z.object({ value: z.number() })),
597
- }),
598
- timestamp: z.string().datetime(),
599
- }),
600
- },
601
- ];
602
-
603
- // json-schema-diff should detect type mismatch (enum vs string)
604
- const result = checker.isImplementedBy(tools);
605
- // Note: json-schema-diff may or may not detect enum vs string differences
606
- expect(typeof result).toBe("boolean");
607
- });
608
-
609
- it("should fail when tool has wrong array element type", () => {
610
- const checker = createBindingChecker(COMPLEX_BINDING);
611
-
612
- const tools = [
613
- {
614
- name: "COMPLEX_TOOL",
615
- inputSchema: z.object({
616
- user: z.object({
617
- id: z.string(),
618
- email: z.string().email(),
619
- profile: z.object({
620
- name: z.string(),
621
- age: z.number().optional(),
622
- }),
623
- }),
624
- tags: z.array(z.number()), // Wrong type: should be string[]
625
- metadata: z.record(z.string(), z.any()),
626
- }),
627
- outputSchema: z.object({
628
- result: z.object({
629
- id: z.string(),
630
- status: z.enum(["success", "error"]),
631
- data: z.array(z.object({ value: z.number() })),
632
- }),
633
- timestamp: z.string().datetime(),
634
- }),
635
- },
636
- ];
637
-
638
- expect(checker.isImplementedBy(tools)).toBe(false);
639
- });
640
- });
641
-
642
- describe("Edge cases for schema validation", () => {
643
- it("should pass when binder has no input schema", () => {
644
- const BINDING_NO_INPUT = [
645
- {
646
- name: "NO_INPUT_TOOL" as const,
647
- inputSchema: z.any(),
648
- outputSchema: z.object({ result: z.string() }),
649
- },
650
- ] as const satisfies Binder;
651
-
652
- const checker = createBindingChecker(BINDING_NO_INPUT);
653
-
654
- const tools = [
655
- {
656
- name: "NO_INPUT_TOOL",
657
- inputSchema: z.object({ anything: z.any() }),
658
- outputSchema: z.object({ result: z.string() }),
659
- },
660
- ];
661
-
662
- // When binder has z.any(), tool should be able to accept anything
663
- // Note: json-schema-diff might handle z.any() differently
664
- const result = checker.isImplementedBy(tools);
665
- expect(typeof result).toBe("boolean");
666
- });
667
-
668
- it("should pass when binder has no output schema", () => {
669
- const BINDING_NO_OUTPUT = [
670
- {
671
- name: "NO_OUTPUT_TOOL" as const,
672
- inputSchema: z.object({ id: z.string() }),
673
- },
674
- ] as const satisfies Binder;
675
-
676
- const checker = createBindingChecker(BINDING_NO_OUTPUT);
677
-
678
- const tools = [
679
- {
680
- name: "NO_OUTPUT_TOOL",
681
- inputSchema: z.object({ id: z.string() }),
682
- outputSchema: z.object({ anything: z.any() }),
683
- },
684
- ];
685
-
686
- expect(checker.isImplementedBy(tools)).toBe(true);
687
- });
688
-
689
- it("should pass when tool input schema accepts union types that include binder type", () => {
690
- const BINDING = [
691
- {
692
- name: "UNION_TOOL" as const,
693
- inputSchema: z.object({ value: z.string() }),
694
- outputSchema: z.object({ result: z.boolean() }),
695
- },
696
- ] as const satisfies Binder;
697
-
698
- const checker = createBindingChecker(BINDING);
699
-
700
- // Tool accepts string | number, which includes string (binder requirement)
701
- const tools = [
702
- {
703
- name: "UNION_TOOL",
704
- inputSchema: z.object({ value: z.union([z.string(), z.number()]) }),
705
- outputSchema: z.object({ result: z.boolean() }),
706
- },
707
- ];
708
-
709
- // Note: This might fail with json-schema-diff if it's strict about unions
710
- // But the intent is that tool should accept what binder requires
711
- const result = checker.isImplementedBy(tools);
712
- // The result depends on how json-schema-diff handles unions
713
- expect(typeof result).toBe("boolean");
714
- });
715
-
716
- it("should handle optional vs required fields correctly", () => {
717
- const BINDING = [
718
- {
719
- name: "OPTIONAL_FIELD_TOOL" as const,
720
- inputSchema: z.object({
721
- required: z.string(),
722
- optional: z.string().optional(),
723
- }),
724
- outputSchema: z.object({
725
- result: z.string(),
726
- extra: z.number().optional(),
727
- }),
728
- },
729
- ] as const satisfies Binder;
730
-
731
- const checker = createBindingChecker(BINDING);
732
-
733
- // Tool that omits optional field - should pass
734
- const tools1 = [
735
- {
736
- name: "OPTIONAL_FIELD_TOOL",
737
- inputSchema: z.object({
738
- required: z.string(),
739
- // Missing optional field is OK
740
- }),
741
- outputSchema: z.object({
742
- result: z.string(),
743
- extra: z.number().optional(),
744
- }),
745
- },
746
- ];
747
-
748
- // Tool missing optional field should pass
749
- const result1 = checker.isImplementedBy(tools1);
750
- expect(typeof result1).toBe("boolean");
751
-
752
- // Tool that requires optional field should also pass (it accepts what binder requires)
753
- const tools2 = [
754
- {
755
- name: "OPTIONAL_FIELD_TOOL",
756
- inputSchema: z.object({
757
- required: z.string(),
758
- optional: z.string(), // Required in tool, optional in binder - should pass
759
- }),
760
- outputSchema: z.object({
761
- result: z.string(),
762
- extra: z.number().optional(),
763
- }),
764
- },
765
- ];
766
-
767
- // Note: json-schema-diff might handle optional->required differently
768
- const result2 = checker.isImplementedBy(tools2);
769
- expect(typeof result2).toBe("boolean");
770
- });
771
-
772
- it("should handle record/object schemas correctly", () => {
773
- const BINDING = [
774
- {
775
- name: "RECORD_TOOL" as const,
776
- inputSchema: z.object({
777
- metadata: z.record(z.string(), z.string()),
778
- }),
779
- outputSchema: z.object({
780
- data: z.record(z.string(), z.any()),
781
- }),
782
- },
783
- ] as const satisfies Binder;
784
-
785
- const checker = createBindingChecker(BINDING);
786
-
787
- // Tool with compatible record schema
788
- const tools = [
789
- {
790
- name: "RECORD_TOOL",
791
- inputSchema: z.object({
792
- metadata: z.record(z.string(), z.any()), // Accepts string values (more permissive)
793
- }),
794
- outputSchema: z.object({
795
- data: z.record(z.string(), z.any()),
796
- }),
797
- },
798
- ];
799
-
800
- expect(checker.isImplementedBy(tools)).toBe(true);
801
- });
802
- });
803
-
804
- describe("Type inference", () => {
805
- it("should infer input types from schemas", () => {
806
- const binding = [
807
- {
808
- name: "TEST_TOOL" as const,
809
- inputSchema: z.object({
810
- id: z.string(),
811
- count: z.number(),
812
- }),
813
- outputSchema: z.object({
814
- success: z.boolean(),
815
- }),
816
- },
817
- ] as const satisfies Binder;
818
-
819
- type InputType = z.infer<(typeof binding)[0]["inputSchema"]>;
820
- type OutputType = z.infer<
821
- NonNullable<(typeof binding)[0]["outputSchema"]>
822
- >;
823
-
824
- const input: InputType = { id: "test", count: 5 };
825
- const output: OutputType = { success: true };
826
-
827
- expect(input.id).toBe("test");
828
- expect(output.success).toBe(true);
829
- });
830
- });
831
-
832
- describe("Real-world binding examples", () => {
833
- it("should work with a channel binding", () => {
834
- const CHANNEL_BINDING = [
835
- {
836
- name: "DECO_CHAT_CHANNELS_JOIN" as const,
837
- inputSchema: z.object({
838
- workspace: z.string(),
839
- discriminator: z.string(),
840
- agentId: z.string(),
841
- }),
842
- outputSchema: z.any(),
843
- },
844
- {
845
- name: "DECO_CHAT_CHANNELS_LEAVE" as const,
846
- inputSchema: z.object({
847
- workspace: z.string(),
848
- discriminator: z.string(),
849
- }),
850
- outputSchema: z.any(),
851
- },
852
- {
853
- name: "DECO_CHAT_CHANNELS_LIST" as const,
854
- inputSchema: z.any(),
855
- outputSchema: z.object({
856
- channels: z.array(
857
- z.object({
858
- label: z.string(),
859
- value: z.string(),
860
- }),
861
- ),
862
- }),
863
- opt: true,
864
- },
865
- ] as const satisfies Binder;
866
-
867
- const checker = createBindingChecker(CHANNEL_BINDING);
868
-
869
- // Should pass with all tools
870
- expect(
871
- checker.isImplementedBy([
872
- {
873
- name: "DECO_CHAT_CHANNELS_JOIN",
874
- inputSchema: z.object({
875
- workspace: z.string(),
876
- discriminator: z.string(),
877
- agentId: z.string(),
878
- }),
879
- outputSchema: z.any(),
880
- },
881
- {
882
- name: "DECO_CHAT_CHANNELS_LEAVE",
883
- inputSchema: z.object({
884
- workspace: z.string(),
885
- discriminator: z.string(),
886
- }),
887
- outputSchema: z.any(),
888
- },
889
- {
890
- name: "DECO_CHAT_CHANNELS_LIST",
891
- inputSchema: z.any(),
892
- outputSchema: z.object({
893
- channels: z.array(
894
- z.object({
895
- label: z.string(),
896
- value: z.string(),
897
- }),
898
- ),
899
- }),
900
- },
901
- ]),
902
- ).toBe(true);
903
-
904
- // Should pass without optional tool
905
- expect(
906
- checker.isImplementedBy([
907
- {
908
- name: "DECO_CHAT_CHANNELS_JOIN",
909
- inputSchema: z.object({
910
- workspace: z.string(),
911
- discriminator: z.string(),
912
- agentId: z.string(),
913
- }),
914
- outputSchema: z.any(),
915
- },
916
- {
917
- name: "DECO_CHAT_CHANNELS_LEAVE",
918
- inputSchema: z.object({
919
- workspace: z.string(),
920
- discriminator: z.string(),
921
- }),
922
- outputSchema: z.any(),
923
- },
924
- ]),
925
- ).toBe(true);
926
-
927
- // Should fail without required tools
928
- expect(
929
- checker.isImplementedBy([
930
- {
931
- name: "DECO_CHAT_CHANNELS_JOIN",
932
- inputSchema: z.object({
933
- workspace: z.string(),
934
- discriminator: z.string(),
935
- agentId: z.string(),
936
- }),
937
- outputSchema: z.any(),
938
- },
939
- ]),
940
- ).toBe(false);
941
- });
942
- });
943
- });