mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
- 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
450 lines
15 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|