Skip to content

Commit bf1dd20

Browse files
committed
test: add unit tests for schema-utils functions toJSONSchema and toToolsJSONSchema.
1 parent c268a66 commit bf1dd20

207 files changed

Lines changed: 387 additions & 5 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
import { describe, expect, it } from "vitest";
2+
import { toJSONSchema, toToolsJSONSchema } from "./schema-utils";
3+
import type { Tool } from "../types/tool-types";
4+
5+
describe("toJSONSchema", () => {
6+
it("converts StandardSchemaV1 with ~standard.toJSONSchema", () => {
7+
const mockStandardSchema = {
8+
"~standard": {
9+
version: 1,
10+
vendor: "test",
11+
validate: () => ({ value: {} }),
12+
toJSONSchema: () => ({
13+
type: "object",
14+
properties: { name: { type: "string" } },
15+
}),
16+
},
17+
};
18+
19+
const result = toJSONSchema(mockStandardSchema);
20+
expect(result).toEqual({
21+
type: "object",
22+
properties: { name: { type: "string" } },
23+
});
24+
});
25+
26+
it("converts object with toJSONSchema() method", () => {
27+
const schemaWithMethod = {
28+
toJSONSchema: () => ({
29+
type: "object",
30+
properties: { age: { type: "number" } },
31+
}),
32+
};
33+
34+
const result = toJSONSchema(schemaWithMethod as never);
35+
expect(result).toEqual({
36+
type: "object",
37+
properties: { age: { type: "number" } },
38+
});
39+
});
40+
41+
it("converts object with toJSON() method", () => {
42+
const schemaWithToJSON = {
43+
toJSON: () => ({
44+
type: "object",
45+
properties: { active: { type: "boolean" } },
46+
}),
47+
};
48+
49+
const result = toJSONSchema(schemaWithToJSON as never);
50+
expect(result).toEqual({
51+
type: "object",
52+
properties: { active: { type: "boolean" } },
53+
});
54+
});
55+
56+
it("passes through plain JSONSchema7", () => {
57+
const plainSchema = {
58+
type: "object" as const,
59+
properties: {
60+
email: { type: "string" as const, format: "email" },
61+
},
62+
required: ["email"],
63+
};
64+
65+
const result = toJSONSchema(plainSchema);
66+
expect(result).toEqual(plainSchema);
67+
});
68+
69+
it("prioritizes StandardSchema over toJSONSchema method", () => {
70+
const mixedSchema = {
71+
"~standard": {
72+
version: 1,
73+
vendor: "test",
74+
validate: () => ({ value: {} }),
75+
toJSONSchema: () => ({ type: "string", description: "from standard" }),
76+
},
77+
toJSONSchema: () => ({ type: "number", description: "from method" }),
78+
};
79+
80+
const result = toJSONSchema(mixedSchema);
81+
expect(result).toEqual({ type: "string", description: "from standard" });
82+
});
83+
84+
it("prioritizes toJSONSchema over toJSON method", () => {
85+
const mixedSchema = {
86+
toJSONSchema: () => ({
87+
type: "string",
88+
description: "from toJSONSchema",
89+
}),
90+
toJSON: () => ({ type: "number", description: "from toJSON" }),
91+
};
92+
93+
const result = toJSONSchema(mixedSchema as never);
94+
expect(result).toEqual({
95+
type: "string",
96+
description: "from toJSONSchema",
97+
});
98+
});
99+
100+
it("falls back to plain schema when StandardSchema has no toJSONSchema", () => {
101+
const schemaWithoutMethod = {
102+
"~standard": {
103+
version: 1,
104+
vendor: "test",
105+
validate: () => ({ value: {} }),
106+
// no toJSONSchema method
107+
},
108+
type: "object" as const,
109+
properties: {},
110+
};
111+
112+
const result = toJSONSchema(schemaWithoutMethod);
113+
// Should return the object as-is since ~standard.toJSONSchema is not a function
114+
expect(result).toEqual(schemaWithoutMethod);
115+
});
116+
});
117+
118+
describe("toToolsJSONSchema", () => {
119+
describe("filtering", () => {
120+
it("excludes disabled tools by default", () => {
121+
const tools: Record<string, Tool> = {
122+
enabledTool: {
123+
description: "Enabled tool",
124+
parameters: { type: "object", properties: {} },
125+
},
126+
disabledTool: {
127+
disabled: true,
128+
description: "Disabled tool",
129+
parameters: { type: "object", properties: {} },
130+
},
131+
};
132+
133+
const result = toToolsJSONSchema(tools);
134+
expect(result).toHaveProperty("enabledTool");
135+
expect(result).not.toHaveProperty("disabledTool");
136+
});
137+
138+
it("excludes backend tools by default", () => {
139+
const tools: Record<string, Tool> = {
140+
frontendTool: {
141+
type: "frontend",
142+
description: "Frontend tool",
143+
parameters: { type: "object", properties: {} },
144+
},
145+
backendTool: {
146+
type: "backend",
147+
},
148+
};
149+
150+
const result = toToolsJSONSchema(tools);
151+
expect(result).toHaveProperty("frontendTool");
152+
expect(result).not.toHaveProperty("backendTool");
153+
});
154+
155+
it("includes frontend tools", () => {
156+
const tools: Record<string, Tool> = {
157+
myTool: {
158+
type: "frontend",
159+
description: "A frontend tool",
160+
parameters: { type: "object", properties: { x: { type: "number" } } },
161+
},
162+
};
163+
164+
const result = toToolsJSONSchema(tools);
165+
expect(result).toEqual({
166+
myTool: {
167+
description: "A frontend tool",
168+
parameters: { type: "object", properties: { x: { type: "number" } } },
169+
},
170+
});
171+
});
172+
173+
it("includes human tools", () => {
174+
const tools: Record<string, Tool> = {
175+
humanTool: {
176+
type: "human",
177+
description: "A human tool",
178+
parameters: { type: "object", properties: {} },
179+
},
180+
};
181+
182+
const result = toToolsJSONSchema(tools);
183+
expect(result).toHaveProperty("humanTool");
184+
});
185+
186+
it("excludes tools without parameters", () => {
187+
const tools: Record<string, Tool> = {
188+
withParams: {
189+
description: "With params",
190+
parameters: { type: "object", properties: {} },
191+
},
192+
withoutParams: {
193+
type: "backend",
194+
},
195+
};
196+
197+
const result = toToolsJSONSchema(tools);
198+
expect(result).toHaveProperty("withParams");
199+
expect(result).not.toHaveProperty("withoutParams");
200+
});
201+
202+
it("respects custom filter function", () => {
203+
const tools: Record<string, Tool> = {
204+
tool_a: {
205+
disabled: true,
206+
parameters: { type: "object", properties: {} },
207+
},
208+
tool_b: {
209+
type: "backend",
210+
},
211+
tool_c: {
212+
parameters: { type: "object", properties: {} },
213+
},
214+
};
215+
216+
// Custom filter that includes all tools regardless of disabled/backend
217+
const result = toToolsJSONSchema(tools, {
218+
filter: () => true,
219+
});
220+
221+
// tool_a and tool_c have parameters, tool_b does not
222+
expect(result).toHaveProperty("tool_a");
223+
expect(result).not.toHaveProperty("tool_b"); // still excluded due to no parameters
224+
expect(result).toHaveProperty("tool_c");
225+
});
226+
227+
it("custom filter receives name and tool", () => {
228+
const tools: Record<string, Tool> = {
229+
prefixed_tool: {
230+
description: "Should include",
231+
parameters: { type: "object", properties: {} },
232+
},
233+
other_tool: {
234+
description: "Should exclude",
235+
parameters: { type: "object", properties: {} },
236+
},
237+
};
238+
239+
const result = toToolsJSONSchema(tools, {
240+
filter: (name, tool) =>
241+
name.startsWith("prefixed_") && tool.description !== undefined,
242+
});
243+
244+
expect(result).toHaveProperty("prefixed_tool");
245+
expect(result).not.toHaveProperty("other_tool");
246+
});
247+
});
248+
249+
describe("output format", () => {
250+
it("includes description when present", () => {
251+
const tools: Record<string, Tool> = {
252+
myTool: {
253+
description: "This is my tool",
254+
parameters: { type: "object", properties: {} },
255+
},
256+
};
257+
258+
const result = toToolsJSONSchema(tools);
259+
expect(result.myTool).toEqual({
260+
description: "This is my tool",
261+
parameters: { type: "object", properties: {} },
262+
});
263+
});
264+
265+
it("omits description when absent", () => {
266+
const tools: Record<string, Tool> = {
267+
myTool: {
268+
parameters: { type: "object", properties: {} },
269+
},
270+
};
271+
272+
const result = toToolsJSONSchema(tools);
273+
expect(result.myTool).toEqual({
274+
parameters: { type: "object", properties: {} },
275+
});
276+
expect(result.myTool).not.toHaveProperty("description");
277+
});
278+
279+
it("omits description when empty string", () => {
280+
const tools: Record<string, Tool> = {
281+
myTool: {
282+
description: "",
283+
parameters: { type: "object", properties: {} },
284+
},
285+
};
286+
287+
const result = toToolsJSONSchema(tools);
288+
expect(result.myTool).not.toHaveProperty("description");
289+
});
290+
291+
it("converts parameters via toJSONSchema", () => {
292+
const mockStandardSchema = {
293+
"~standard": {
294+
version: 1,
295+
vendor: "test",
296+
validate: () => ({ value: {} }),
297+
toJSONSchema: () => ({
298+
type: "object",
299+
properties: { converted: { type: "boolean" } },
300+
}),
301+
},
302+
};
303+
304+
const tools: Record<string, Tool> = {
305+
myTool: {
306+
description: "Test",
307+
parameters: mockStandardSchema,
308+
},
309+
};
310+
311+
const result = toToolsJSONSchema(tools);
312+
expect(result.myTool!.parameters).toEqual({
313+
type: "object",
314+
properties: { converted: { type: "boolean" } },
315+
});
316+
});
317+
});
318+
319+
describe("edge cases", () => {
320+
it("returns empty object for undefined tools", () => {
321+
const result = toToolsJSONSchema(undefined);
322+
expect(result).toEqual({});
323+
});
324+
325+
it("returns empty object for empty tools", () => {
326+
const result = toToolsJSONSchema({});
327+
expect(result).toEqual({});
328+
});
329+
330+
it("returns empty object when all tools are filtered out", () => {
331+
const tools: Record<string, Tool> = {
332+
disabled1: {
333+
disabled: true,
334+
parameters: { type: "object", properties: {} },
335+
},
336+
disabled2: {
337+
disabled: true,
338+
parameters: { type: "object", properties: {} },
339+
},
340+
};
341+
342+
const result = toToolsJSONSchema(tools);
343+
expect(result).toEqual({});
344+
});
345+
346+
it("handles tools with undefined type (defaults to frontend behavior)", () => {
347+
const tools: Record<string, Tool> = {
348+
myTool: {
349+
// no type specified
350+
description: "Tool without type",
351+
parameters: { type: "object", properties: {} },
352+
},
353+
};
354+
355+
const result = toToolsJSONSchema(tools);
356+
expect(result).toHaveProperty("myTool");
357+
});
358+
359+
it("handles multiple tools correctly", () => {
360+
const tools: Record<string, Tool> = {
361+
tool_1: {
362+
description: "First tool",
363+
parameters: { type: "object", properties: { a: { type: "string" } } },
364+
},
365+
tool_2: {
366+
description: "Second tool",
367+
parameters: { type: "object", properties: { b: { type: "number" } } },
368+
},
369+
tool_3: {
370+
disabled: true,
371+
parameters: { type: "object", properties: {} },
372+
},
373+
};
374+
375+
const result = toToolsJSONSchema(tools);
376+
expect(Object.keys(result)).toHaveLength(2);
377+
expect(result).toHaveProperty("tool_1");
378+
expect(result).toHaveProperty("tool_2");
379+
expect(result).not.toHaveProperty("tool_3");
380+
});
381+
});
382+
});

packages-test-3/ai-chat/src/adapters/empty-storage-adapter.ts renamed to packages/ai-chat/src/adapters/empty-storage-adapter.ts

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)