1import { describe, expect, it } from "vitest"; 2 3import { validateToolParameters, validateTools } from "./tools.js"; 4 5const goodObject = { 6 type: "object", 7 properties: { 8 query: { type: "string", description: "search text" }, 9 limit: { type: "integer" }, 10 tags: { type: "array", items: { type: "string" } }, 11 meta: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, 12 }, 13 required: ["query"], 14}; 15 16describe("validateToolParameters", () => { 17 it("accepts the strict subset (object root, nested, array items, enum)", () => { 18 expect(validateToolParameters(goodObject).ok).toBe(true); 19 expect( 20 validateToolParameters({ 21 type: "object", 22 properties: { kind: { type: "string", enum: ["a", "b"] }, flag: { type: "boolean" }, n: { type: "number" } }, 23 }).ok, 24 ).toBe(true); 25 }); 26 27 it("rejects a non-object root", () => { 28 expect(validateToolParameters("nope").ok).toBe(false); 29 expect(validateToolParameters(null).ok).toBe(false); 30 expect(validateToolParameters([1]).ok).toBe(false); 31 }); 32 33 it("rejects forbidden keywords (pattern, $ref, anyOf, minLength)", () => { 34 expect(validateToolParameters({ type: "object", pattern: "x" }).ok).toBe(false); 35 expect(validateToolParameters({ $ref: "#/foo" }).ok).toBe(false); 36 expect(validateToolParameters({ anyOf: [{}] }).ok).toBe(false); 37 expect(validateToolParameters({ type: "string", minLength: 1 }).ok).toBe(false); 38 }); 39 40 it("rejects unknown types", () => { 41 expect(validateToolParameters({ type: "null" }).ok).toBe(false); 42 expect(validateToolParameters({ type: "date" }).ok).toBe(false); 43 }); 44 45 it("array requires items", () => { 46 expect(validateToolParameters({ type: "array" }).ok).toBe(false); 47 expect(validateToolParameters({ type: "array", items: { type: "string" } }).ok).toBe(true); 48 }); 49 50 it("nested properties are validated recursively", () => { 51 expect( 52 validateToolParameters({ 53 type: "object", 54 properties: { inner: { type: "object", properties: { bad: { pattern: "x" } } } }, 55 }).ok, 56 ).toBe(false); 57 }); 58 59 it("required must reference existing properties", () => { 60 expect( 61 validateToolParameters({ type: "object", properties: { a: { type: "string" } }, required: ["zzz"] }).ok, 62 ).toBe(false); 63 }); 64 65 it("enum must be a non-empty array", () => { 66 expect(validateToolParameters({ type: "string", enum: [] }).ok).toBe(false); 67 }); 68 69 it("error messages carry the path", () => { 70 const r = validateToolParameters({ type: "object", properties: { a: { pattern: "x" } } }); 71 expect(r.ok).toBe(false); 72 expect(r.error).toContain("properties.a"); 73 }); 74}); 75 76describe("validateTools", () => { 77 it("accepts a valid tool list", () => { 78 expect( 79 validateTools([ 80 { name: "search", description: "search the docs", parameters: goodObject }, 81 { name: "get-page_2", description: "get a page", parameters: { type: "object" } }, 82 ]).ok, 83 ).toBe(true); 84 }); 85 86 it("rejects bad tool names", () => { 87 expect(validateTools([{ name: "has space", description: "d", parameters: { type: "object" } }]).ok).toBe(false); 88 expect(validateTools([{ name: "", description: "d", parameters: { type: "object" } }]).ok).toBe(false); 89 }); 90 91 it("rejects duplicate names", () => { 92 expect( 93 validateTools([ 94 { name: "x", description: "d", parameters: { type: "object" } }, 95 { name: "x", description: "d", parameters: { type: "object" } }, 96 ]).ok, 97 ).toBe(false); 98 }); 99 100 it("requires a non-empty description", () => { 101 expect(validateTools([{ name: "x", description: "", parameters: { type: "object" } }]).ok).toBe(false); 102 }); 103 104 it("propagates parameter errors with the tool name", () => { 105 const r = validateTools([{ name: "x", description: "d", parameters: { pattern: "bad" } }]); 106 expect(r.ok).toBe(false); 107 expect(r.error).toContain("tool.x.parameters"); 108 }); 109}); 110