/** * KHAELOR * File: tests/tools/registry.test.ts * Description: Unit tests for the ToolRegistry — registration, schema export, and input validation. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { ToolRegistry, createDefaultToolRegistry, createReadTool, parseToolInput, } from "../../src/tools/index.js"; describe("ToolRegistry", () => { it("registers, lists, and gets tools", () => { const registry = new ToolRegistry(); registry.register(createReadTool()); expect(registry.list().map((t) => t.name)).toEqual(["read"]); expect(registry.get("read")?.name).toBe("read"); expect(registry.get("missing")).toBeUndefined(); }); it("rejects duplicate registration", () => { const registry = new ToolRegistry(); registry.register(createReadTool()); expect(() => registry.register(createReadTool())).toThrowError(/already registered/); }); it("registers the seven V1 tools plus the four v2 tools", () => { const registry = createDefaultToolRegistry(); expect(registry.list().map((t) => t.name)).toEqual([ "read", "write", "edit", "grep", "glob", "bash", "process", "design", "remember", "symbols", "refs", ]); }); it("exports valid Anthropic tool params with at most 5 parameters each", () => { const params = createDefaultToolRegistry().toAnthropicTools(); expect(params).toHaveLength(11); for (const param of params) { expect(typeof param.name).toBe("string"); expect(param.description.length).toBeGreaterThan(50); expect(param.input_schema.type).toBe("object"); const propNames = Object.keys(param.input_schema.properties); expect(propNames.length).toBeGreaterThan(0); expect(propNames.length).toBeLessThanOrEqual(5); for (const required of param.input_schema.required) { expect(propNames).toContain(required); } for (const prop of Object.values(param.input_schema.properties)) { expect(["string", "integer", "boolean"]).toContain(prop.type); expect(prop.description.length).toBeGreaterThan(0); } // Round-trips as plain JSON (what the Anthropic SDK serializes). expect(JSON.parse(JSON.stringify(param))).toEqual(param); } }); it("snake_case parameter naming throughout", () => { for (const param of createDefaultToolRegistry().toAnthropicTools()) { for (const name of Object.keys(param.input_schema.properties)) { expect(name).toMatch(/^[a-z][a-z0-9_]*$/); } } }); }); describe("parseToolInput", () => { const read = createReadTool(); it("accepts valid input", () => { const parsed = parseToolInput(read, { file_path: "a.ts", offset: 2 }); expect(parsed.ok).toBe(true); }); it("produces repair prose for a missing required parameter", () => { const parsed = parseToolInput(read, { offset: 2 }); expect(parsed.ok).toBe(false); if (!parsed.ok) { expect(parsed.error).toBe( 'Invalid input for tool "read": parameter "file_path" is required. Please rewrite the input so it satisfies the expected schema.', ); } }); it("rejects wrong types with repair prose", () => { const parsed = parseToolInput(read, { file_path: "a.ts", offset: "two" }); expect(parsed.ok).toBe(false); if (!parsed.ok) expect(parsed.error).toContain('parameter "offset" must be an integer'); }); it("rejects non-object input", () => { const parsed = parseToolInput(read, "just a string"); expect(parsed.ok).toBe(false); if (!parsed.ok) expect(parsed.error).toContain("must be a JSON object"); }); it("enforces enums (process action)", () => { const registry = createDefaultToolRegistry(); const processTool = registry.get("process"); expect(processTool).toBeDefined(); const parsed = parseToolInput(processTool!, { action: "restart" }); expect(parsed.ok).toBe(false); if (!parsed.ok) { expect(parsed.error).toContain('parameter "action" must be one of "start", "list", "read", "write", "stop"'); } }); });