import { type Static, Type } from "@sinclair/typebox"; import AjvModule from "ajv"; import addFormatsModule from "ajv-formats"; // Handle both default and named exports const Ajv = (AjvModule as any).default || AjvModule; const addFormats = (addFormatsModule as any).default || addFormatsModule; import { describe, expect, it } from "vitest"; import type { AgentTool } from "../src/agent/types.js"; describe("Tool Validation with TypeBox and AJV", () => { // Define a test tool with TypeBox schema const testSchema = Type.Object({ name: Type.String({ minLength: 1 }), age: Type.Integer({ minimum: 0, maximum: 150 }), email: Type.String({ format: "email" }), tags: Type.Optional(Type.Array(Type.String())), }); type TestParams = Static; const testTool: AgentTool = { label: "Test Tool", name: "test_tool", description: "A test tool for validation", parameters: testSchema, execute: async (_toolCallId, args) => { return { content: [{ type: "text", text: `Processed: ${args.name}, ${args.age}, ${args.email}` }], details: undefined, }; }, }; // Create AJV instance for validation const ajv = new Ajv({ allErrors: true }); addFormats(ajv); it("should validate correct input", () => { const validInput = { name: "John Doe", age: 30, email: "john@example.com", tags: ["developer", "typescript"], }; // Validate with AJV const validate = ajv.compile(testTool.parameters); const isValid = validate(validInput); expect(isValid).toBe(true); }); it("should reject invalid email", () => { const invalidInput = { name: "John Doe", age: 30, email: "not-an-email", }; const validate = ajv.compile(testTool.parameters); const isValid = validate(invalidInput); expect(isValid).toBe(false); expect(validate.errors).toBeDefined(); }); it("should reject missing required fields", () => { const invalidInput = { age: 30, email: "john@example.com", }; const validate = ajv.compile(testTool.parameters); const isValid = validate(invalidInput); expect(isValid).toBe(false); expect(validate.errors).toBeDefined(); }); it("should reject invalid age", () => { const invalidInput = { name: "John Doe", age: -5, email: "john@example.com", }; const validate = ajv.compile(testTool.parameters); const isValid = validate(invalidInput); expect(isValid).toBe(false); expect(validate.errors).toBeDefined(); }); it("should format validation errors nicely", () => { const invalidInput = { name: "", age: 200, email: "invalid", }; const validate = ajv.compile(testTool.parameters); const isValid = validate(invalidInput); expect(isValid).toBe(false); expect(validate.errors).toBeDefined(); if (validate.errors) { const errors = validate.errors .map((err: any) => { const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root"; return ` - ${path}: ${err.message}`; }) .join("\n"); // AJV error messages are different from Zod expect(errors).toContain("name: must NOT have fewer than 1 characters"); expect(errors).toContain("age: must be <= 150"); expect(errors).toContain('email: must match format "email"'); } }); it("should have type-safe execute function", async () => { const validInput = { name: "John Doe", age: 30, email: "john@example.com", }; // Validate and execute const validate = ajv.compile(testTool.parameters); const isValid = validate(validInput); expect(isValid).toBe(true); const result = await testTool.execute("test-id", validInput as TestParams); const textOutput = result.content .filter((c: any) => c.type === "text") .map((c: any) => c.text) .join("\n"); expect(textOutput).toBe("Processed: John Doe, 30, john@example.com"); expect(result.details).toBeUndefined(); }); });