Files
puaros/packages/ipuaro/tests/unit/infrastructure/tools/registry.test.ts
imfozilbek 25146003cc feat(ipuaro): add read tools (v0.5.0)
- ToolRegistry: tool lifecycle management, execution with validation
- GetLinesTool: read file lines with line numbers
- GetFunctionTool: get function source using AST
- GetClassTool: get class source using AST
- GetStructureTool: directory tree with filtering

121 new tests, 540 total
2025-12-01 00:52:00 +05:00

450 lines
15 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest"
import { ToolRegistry } from "../../../../src/infrastructure/tools/registry.js"
import type {
ITool,
ToolContext,
ToolParameterSchema,
} from "../../../../src/domain/services/ITool.js"
import type { ToolResult } from "../../../../src/domain/value-objects/ToolResult.js"
import { IpuaroError } from "../../../../src/shared/errors/IpuaroError.js"
/**
* Creates a mock tool for testing.
*/
function createMockTool(overrides: Partial<ITool> = {}): ITool {
return {
name: "mock_tool",
description: "A mock tool for testing",
parameters: [
{
name: "path",
type: "string",
description: "File path",
required: true,
},
{
name: "optional",
type: "number",
description: "Optional param",
required: false,
},
],
requiresConfirmation: false,
category: "read",
execute: vi.fn().mockResolvedValue({
callId: "test-123",
success: true,
data: { result: "success" },
executionTimeMs: 10,
}),
validateParams: vi.fn().mockReturnValue(null),
...overrides,
}
}
/**
* Creates a mock tool context for testing.
*/
function createMockContext(overrides: Partial<ToolContext> = {}): ToolContext {
return {
projectRoot: "/test/project",
storage: {} as ToolContext["storage"],
requestConfirmation: vi.fn().mockResolvedValue(true),
onProgress: vi.fn(),
...overrides,
}
}
describe("ToolRegistry", () => {
let registry: ToolRegistry
beforeEach(() => {
registry = new ToolRegistry()
})
describe("register", () => {
it("should register a tool", () => {
const tool = createMockTool()
registry.register(tool)
expect(registry.has("mock_tool")).toBe(true)
expect(registry.size).toBe(1)
})
it("should register multiple tools", () => {
const tool1 = createMockTool({ name: "tool_1" })
const tool2 = createMockTool({ name: "tool_2" })
registry.register(tool1)
registry.register(tool2)
expect(registry.size).toBe(2)
expect(registry.has("tool_1")).toBe(true)
expect(registry.has("tool_2")).toBe(true)
})
it("should throw error when registering duplicate tool name", () => {
const tool1 = createMockTool({ name: "duplicate" })
const tool2 = createMockTool({ name: "duplicate" })
registry.register(tool1)
expect(() => registry.register(tool2)).toThrow(IpuaroError)
expect(() => registry.register(tool2)).toThrow('Tool "duplicate" is already registered')
})
})
describe("unregister", () => {
it("should remove a registered tool", () => {
const tool = createMockTool()
registry.register(tool)
const result = registry.unregister("mock_tool")
expect(result).toBe(true)
expect(registry.has("mock_tool")).toBe(false)
expect(registry.size).toBe(0)
})
it("should return false when tool not found", () => {
const result = registry.unregister("nonexistent")
expect(result).toBe(false)
})
})
describe("get", () => {
it("should return registered tool", () => {
const tool = createMockTool()
registry.register(tool)
const result = registry.get("mock_tool")
expect(result).toBe(tool)
})
it("should return undefined for unknown tool", () => {
const result = registry.get("unknown")
expect(result).toBeUndefined()
})
})
describe("getAll", () => {
it("should return empty array when no tools registered", () => {
const result = registry.getAll()
expect(result).toEqual([])
})
it("should return all registered tools", () => {
const tool1 = createMockTool({ name: "tool_1" })
const tool2 = createMockTool({ name: "tool_2" })
registry.register(tool1)
registry.register(tool2)
const result = registry.getAll()
expect(result).toHaveLength(2)
expect(result).toContain(tool1)
expect(result).toContain(tool2)
})
})
describe("getByCategory", () => {
it("should return tools by category", () => {
const readTool = createMockTool({ name: "read_tool", category: "read" })
const editTool = createMockTool({ name: "edit_tool", category: "edit" })
const gitTool = createMockTool({ name: "git_tool", category: "git" })
registry.register(readTool)
registry.register(editTool)
registry.register(gitTool)
const readTools = registry.getByCategory("read")
const editTools = registry.getByCategory("edit")
expect(readTools).toHaveLength(1)
expect(readTools[0]).toBe(readTool)
expect(editTools).toHaveLength(1)
expect(editTools[0]).toBe(editTool)
})
it("should return empty array for category with no tools", () => {
const readTool = createMockTool({ category: "read" })
registry.register(readTool)
const result = registry.getByCategory("analysis")
expect(result).toEqual([])
})
})
describe("has", () => {
it("should return true for registered tool", () => {
registry.register(createMockTool())
expect(registry.has("mock_tool")).toBe(true)
})
it("should return false for unknown tool", () => {
expect(registry.has("unknown")).toBe(false)
})
})
describe("execute", () => {
it("should execute tool and return result", async () => {
const tool = createMockTool()
registry.register(tool)
const ctx = createMockContext()
const result = await registry.execute("mock_tool", { path: "test.ts" }, ctx)
expect(result.success).toBe(true)
expect(result.data).toEqual({ result: "success" })
expect(tool.execute).toHaveBeenCalledWith({ path: "test.ts" }, ctx)
})
it("should return error result for unknown tool", async () => {
const ctx = createMockContext()
const result = await registry.execute("unknown", {}, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe('Tool "unknown" not found')
})
it("should return error result when validation fails", async () => {
const tool = createMockTool({
validateParams: vi.fn().mockReturnValue("Missing required param: path"),
})
registry.register(tool)
const ctx = createMockContext()
const result = await registry.execute("mock_tool", {}, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Missing required param: path")
expect(tool.execute).not.toHaveBeenCalled()
})
it("should request confirmation for tools that require it", async () => {
const tool = createMockTool({ requiresConfirmation: true })
registry.register(tool)
const ctx = createMockContext()
await registry.execute("mock_tool", { path: "test.ts" }, ctx)
expect(ctx.requestConfirmation).toHaveBeenCalled()
expect(tool.execute).toHaveBeenCalled()
})
it("should not execute when confirmation is denied", async () => {
const tool = createMockTool({ requiresConfirmation: true })
registry.register(tool)
const ctx = createMockContext({
requestConfirmation: vi.fn().mockResolvedValue(false),
})
const result = await registry.execute("mock_tool", { path: "test.ts" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("User cancelled operation")
expect(tool.execute).not.toHaveBeenCalled()
})
it("should not request confirmation for safe tools", async () => {
const tool = createMockTool({ requiresConfirmation: false })
registry.register(tool)
const ctx = createMockContext()
await registry.execute("mock_tool", { path: "test.ts" }, ctx)
expect(ctx.requestConfirmation).not.toHaveBeenCalled()
expect(tool.execute).toHaveBeenCalled()
})
it("should catch and return errors from tool execution", async () => {
const tool = createMockTool({
execute: vi.fn().mockRejectedValue(new Error("Execution failed")),
})
registry.register(tool)
const ctx = createMockContext()
const result = await registry.execute("mock_tool", { path: "test.ts" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Execution failed")
})
it("should include callId in result", async () => {
const tool = createMockTool()
registry.register(tool)
const ctx = createMockContext()
const result = await registry.execute("mock_tool", { path: "test.ts" }, ctx)
expect(result.callId).toMatch(/^mock_tool-\d+$/)
})
})
describe("getToolDefinitions", () => {
it("should return empty array when no tools registered", () => {
const result = registry.getToolDefinitions()
expect(result).toEqual([])
})
it("should convert tools to LLM-compatible format", () => {
const tool = createMockTool()
registry.register(tool)
const result = registry.getToolDefinitions()
expect(result).toHaveLength(1)
expect(result[0]).toEqual({
name: "mock_tool",
description: "A mock tool for testing",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "File path",
},
optional: {
type: "number",
description: "Optional param",
},
},
required: ["path"],
},
})
})
it("should handle tools with no parameters", () => {
const tool = createMockTool({ parameters: [] })
registry.register(tool)
const result = registry.getToolDefinitions()
expect(result[0].parameters).toEqual({
type: "object",
properties: {},
required: [],
})
})
it("should handle multiple tools", () => {
registry.register(createMockTool({ name: "tool_1" }))
registry.register(createMockTool({ name: "tool_2" }))
const result = registry.getToolDefinitions()
expect(result).toHaveLength(2)
expect(result.map((t) => t.name)).toEqual(["tool_1", "tool_2"])
})
})
describe("clear", () => {
it("should remove all tools", () => {
registry.register(createMockTool({ name: "tool_1" }))
registry.register(createMockTool({ name: "tool_2" }))
registry.clear()
expect(registry.size).toBe(0)
expect(registry.getAll()).toEqual([])
})
})
describe("getNames", () => {
it("should return all tool names", () => {
registry.register(createMockTool({ name: "alpha" }))
registry.register(createMockTool({ name: "beta" }))
const result = registry.getNames()
expect(result).toEqual(["alpha", "beta"])
})
it("should return empty array when no tools", () => {
const result = registry.getNames()
expect(result).toEqual([])
})
})
describe("getConfirmationTools", () => {
it("should return only tools requiring confirmation", () => {
registry.register(createMockTool({ name: "safe", requiresConfirmation: false }))
registry.register(createMockTool({ name: "dangerous", requiresConfirmation: true }))
registry.register(createMockTool({ name: "also_safe", requiresConfirmation: false }))
const result = registry.getConfirmationTools()
expect(result).toHaveLength(1)
expect(result[0].name).toBe("dangerous")
})
})
describe("getSafeTools", () => {
it("should return only tools not requiring confirmation", () => {
registry.register(createMockTool({ name: "safe", requiresConfirmation: false }))
registry.register(createMockTool({ name: "dangerous", requiresConfirmation: true }))
registry.register(createMockTool({ name: "also_safe", requiresConfirmation: false }))
const result = registry.getSafeTools()
expect(result).toHaveLength(2)
expect(result.map((t) => t.name)).toEqual(["safe", "also_safe"])
})
})
describe("size", () => {
it("should return 0 for empty registry", () => {
expect(registry.size).toBe(0)
})
it("should return correct count", () => {
registry.register(createMockTool({ name: "a" }))
registry.register(createMockTool({ name: "b" }))
registry.register(createMockTool({ name: "c" }))
expect(registry.size).toBe(3)
})
})
describe("integration scenarios", () => {
it("should handle full workflow: register, execute, unregister", async () => {
const tool = createMockTool()
const ctx = createMockContext()
registry.register(tool)
expect(registry.has("mock_tool")).toBe(true)
const result = await registry.execute("mock_tool", { path: "test.ts" }, ctx)
expect(result.success).toBe(true)
registry.unregister("mock_tool")
expect(registry.has("mock_tool")).toBe(false)
const afterUnregister = await registry.execute("mock_tool", {}, ctx)
expect(afterUnregister.success).toBe(false)
})
it("should maintain isolation between registrations", () => {
const registry1 = new ToolRegistry()
const registry2 = new ToolRegistry()
registry1.register(createMockTool({ name: "tool_1" }))
registry2.register(createMockTool({ name: "tool_2" }))
expect(registry1.has("tool_1")).toBe(true)
expect(registry1.has("tool_2")).toBe(false)
expect(registry2.has("tool_1")).toBe(false)
expect(registry2.has("tool_2")).toBe(true)
})
})
})