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