feat(ipuaro): add LLM integration module

- OllamaClient: ILLMClient implementation with tool support
- System prompt and context builders for project overview
- 18 tool definitions across 6 categories (read, edit, search, analysis, git, run)
- XML response parser for tool call extraction
- 98 new tests (419 total), 96.38% coverage
This commit is contained in:
imfozilbek
2025-12-01 00:10:11 +05:00
parent b3e04a411c
commit 68f927d906
12 changed files with 2594 additions and 1 deletions

View File

@@ -0,0 +1,304 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import type { LLMConfig } from "../../../../src/shared/constants/config.js"
import { IpuaroError } from "../../../../src/shared/errors/IpuaroError.js"
import { createUserMessage } from "../../../../src/domain/value-objects/ChatMessage.js"
const mockChatResponse = {
message: {
role: "assistant",
content: "This is a test response.",
tool_calls: undefined,
},
eval_count: 50,
done_reason: "stop",
}
const mockListResponse = {
models: [
{ name: "qwen2.5-coder:7b-instruct", size: 4000000000 },
{ name: "llama2:latest", size: 3500000000 },
],
}
const mockOllamaInstance = {
chat: vi.fn(),
list: vi.fn(),
pull: vi.fn(),
}
vi.mock("ollama", () => {
return {
Ollama: vi.fn(() => mockOllamaInstance),
}
})
const { OllamaClient } = await import("../../../../src/infrastructure/llm/OllamaClient.js")
describe("OllamaClient", () => {
const defaultConfig: LLMConfig = {
model: "qwen2.5-coder:7b-instruct",
contextWindow: 128000,
temperature: 0.1,
host: "http://localhost:11434",
timeout: 120000,
}
beforeEach(() => {
vi.clearAllMocks()
mockOllamaInstance.chat.mockResolvedValue(mockChatResponse)
mockOllamaInstance.list.mockResolvedValue(mockListResponse)
mockOllamaInstance.pull.mockResolvedValue({})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe("constructor", () => {
it("should create instance with config", () => {
const client = new OllamaClient(defaultConfig)
expect(client).toBeDefined()
expect(client.getModelName()).toBe("qwen2.5-coder:7b-instruct")
expect(client.getContextWindowSize()).toBe(128000)
})
})
describe("chat", () => {
it("should send messages and return response", async () => {
const client = new OllamaClient(defaultConfig)
const messages = [createUserMessage("Hello, world!")]
const response = await client.chat(messages)
expect(response.content).toBe("This is a test response.")
expect(response.tokens).toBe(50)
expect(response.stopReason).toBe("end")
expect(response.truncated).toBe(false)
})
it("should convert messages to Ollama format", async () => {
const client = new OllamaClient(defaultConfig)
const messages = [createUserMessage("Hello")]
await client.chat(messages)
expect(mockOllamaInstance.chat).toHaveBeenCalledWith(
expect.objectContaining({
model: "qwen2.5-coder:7b-instruct",
messages: expect.arrayContaining([
expect.objectContaining({
role: "user",
content: "Hello",
}),
]),
}),
)
})
it("should pass tools when provided", async () => {
const client = new OllamaClient(defaultConfig)
const messages = [createUserMessage("Read file")]
const tools = [
{
name: "get_lines",
description: "Get lines from file",
parameters: [
{
name: "path",
type: "string" as const,
description: "File path",
required: true,
},
],
},
]
await client.chat(messages, tools)
expect(mockOllamaInstance.chat).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.arrayContaining([
expect.objectContaining({
type: "function",
function: expect.objectContaining({
name: "get_lines",
}),
}),
]),
}),
)
})
it("should extract tool calls from response", async () => {
mockOllamaInstance.chat.mockResolvedValue({
message: {
role: "assistant",
content: "",
tool_calls: [
{
function: {
name: "get_lines",
arguments: { path: "src/index.ts" },
},
},
],
},
eval_count: 30,
})
const client = new OllamaClient(defaultConfig)
const response = await client.chat([createUserMessage("Read file")])
expect(response.toolCalls).toHaveLength(1)
expect(response.toolCalls[0].name).toBe("get_lines")
expect(response.toolCalls[0].params).toEqual({ path: "src/index.ts" })
expect(response.stopReason).toBe("tool_use")
})
it("should handle connection errors", async () => {
mockOllamaInstance.chat.mockRejectedValue(new Error("fetch failed"))
const client = new OllamaClient(defaultConfig)
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(IpuaroError)
})
it("should handle model not found errors", async () => {
mockOllamaInstance.chat.mockRejectedValue(new Error("model not found"))
const client = new OllamaClient(defaultConfig)
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(/not found/)
})
})
describe("countTokens", () => {
it("should estimate tokens for text", async () => {
const client = new OllamaClient(defaultConfig)
const count = await client.countTokens("Hello, world!")
expect(count).toBeGreaterThan(0)
expect(typeof count).toBe("number")
})
})
describe("isAvailable", () => {
it("should return true when Ollama is available", async () => {
const client = new OllamaClient(defaultConfig)
const available = await client.isAvailable()
expect(available).toBe(true)
})
it("should return false when Ollama is not available", async () => {
mockOllamaInstance.list.mockRejectedValue(new Error("Connection refused"))
const client = new OllamaClient(defaultConfig)
const available = await client.isAvailable()
expect(available).toBe(false)
})
})
describe("getModelName", () => {
it("should return configured model name", () => {
const client = new OllamaClient(defaultConfig)
expect(client.getModelName()).toBe("qwen2.5-coder:7b-instruct")
})
})
describe("getContextWindowSize", () => {
it("should return configured context window size", () => {
const client = new OllamaClient(defaultConfig)
expect(client.getContextWindowSize()).toBe(128000)
})
})
describe("pullModel", () => {
it("should pull model successfully", async () => {
const client = new OllamaClient(defaultConfig)
await expect(client.pullModel("llama2")).resolves.toBeUndefined()
expect(mockOllamaInstance.pull).toHaveBeenCalledWith({
model: "llama2",
stream: false,
})
})
it("should throw on pull failure", async () => {
mockOllamaInstance.pull.mockRejectedValue(new Error("Network error"))
const client = new OllamaClient(defaultConfig)
await expect(client.pullModel("llama2")).rejects.toThrow(IpuaroError)
})
})
describe("hasModel", () => {
it("should return true for available model", async () => {
const client = new OllamaClient(defaultConfig)
const has = await client.hasModel("qwen2.5-coder:7b-instruct")
expect(has).toBe(true)
})
it("should return true for model prefix", async () => {
const client = new OllamaClient(defaultConfig)
const has = await client.hasModel("llama2")
expect(has).toBe(true)
})
it("should return false for missing model", async () => {
const client = new OllamaClient(defaultConfig)
const has = await client.hasModel("unknown-model")
expect(has).toBe(false)
})
it("should return false when list fails", async () => {
mockOllamaInstance.list.mockRejectedValue(new Error("Error"))
const client = new OllamaClient(defaultConfig)
const has = await client.hasModel("any-model")
expect(has).toBe(false)
})
})
describe("listModels", () => {
it("should return list of model names", async () => {
const client = new OllamaClient(defaultConfig)
const models = await client.listModels()
expect(models).toContain("qwen2.5-coder:7b-instruct")
expect(models).toContain("llama2:latest")
})
it("should throw on list failure", async () => {
mockOllamaInstance.list.mockRejectedValue(new Error("Network error"))
const client = new OllamaClient(defaultConfig)
await expect(client.listModels()).rejects.toThrow(IpuaroError)
})
})
describe("abort", () => {
it("should not throw when no request is in progress", () => {
const client = new OllamaClient(defaultConfig)
expect(() => client.abort()).not.toThrow()
})
})
})

View File

@@ -0,0 +1,255 @@
import { describe, it, expect } from "vitest"
import {
parseToolCalls,
formatToolCallsAsXml,
extractThinking,
hasToolCalls,
validateToolCallParams,
} from "../../../../src/infrastructure/llm/ResponseParser.js"
import { createToolCall } from "../../../../src/domain/value-objects/ToolCall.js"
describe("ResponseParser", () => {
describe("parseToolCalls", () => {
it("should parse a single tool call", () => {
const response = `<tool_call name="get_lines">
<path>src/index.ts</path>
<start>1</start>
<end>10</end>
</tool_call>`
const result = parseToolCalls(response)
expect(result.toolCalls).toHaveLength(1)
expect(result.toolCalls[0].name).toBe("get_lines")
expect(result.toolCalls[0].params).toEqual({
path: "src/index.ts",
start: 1,
end: 10,
})
expect(result.hasParseErrors).toBe(false)
})
it("should parse multiple tool calls", () => {
const response = `
<tool_call name="get_lines">
<path>src/a.ts</path>
</tool_call>
<tool_call name="get_function">
<path>src/b.ts</path>
<name>myFunc</name>
</tool_call>
`
const result = parseToolCalls(response)
expect(result.toolCalls).toHaveLength(2)
expect(result.toolCalls[0].name).toBe("get_lines")
expect(result.toolCalls[1].name).toBe("get_function")
})
it("should extract text content without tool calls", () => {
const response = `Let me check the file.
<tool_call name="get_lines">
<path>src/index.ts</path>
</tool_call>
Here's what I found.`
const result = parseToolCalls(response)
expect(result.content).toContain("Let me check the file.")
expect(result.content).toContain("Here's what I found.")
expect(result.content).not.toContain("tool_call")
})
it("should parse boolean values", () => {
const response = `<tool_call name="git_diff">
<staged>true</staged>
</tool_call>`
const result = parseToolCalls(response)
expect(result.toolCalls[0].params.staged).toBe(true)
})
it("should parse null values", () => {
const response = `<tool_call name="test">
<value>null</value>
</tool_call>`
const result = parseToolCalls(response)
expect(result.toolCalls[0].params.value).toBe(null)
})
it("should parse JSON arrays", () => {
const response = `<tool_call name="git_commit">
<files>["a.ts", "b.ts"]</files>
</tool_call>`
const result = parseToolCalls(response)
expect(result.toolCalls[0].params.files).toEqual(["a.ts", "b.ts"])
})
it("should parse JSON objects", () => {
const response = `<tool_call name="test">
<config>{"key": "value"}</config>
</tool_call>`
const result = parseToolCalls(response)
expect(result.toolCalls[0].params.config).toEqual({ key: "value" })
})
it("should return empty array for response without tool calls", () => {
const response = "This is just a regular response."
const result = parseToolCalls(response)
expect(result.toolCalls).toHaveLength(0)
expect(result.content).toBe(response)
})
it("should handle named param syntax", () => {
const response = `<tool_call name="get_lines">
<param name="path">src/index.ts</param>
<param name="start">5</param>
</tool_call>`
const result = parseToolCalls(response)
expect(result.toolCalls[0].params).toEqual({
path: "src/index.ts",
start: 5,
})
})
})
describe("formatToolCallsAsXml", () => {
it("should format tool calls as XML", () => {
const toolCalls = [
createToolCall("1", "get_lines", { path: "src/index.ts", start: 1 }),
]
const xml = formatToolCallsAsXml(toolCalls)
expect(xml).toContain('<tool_call name="get_lines">')
expect(xml).toContain("<path>src/index.ts</path>")
expect(xml).toContain("<start>1</start>")
expect(xml).toContain("</tool_call>")
})
it("should format multiple tool calls", () => {
const toolCalls = [
createToolCall("1", "get_lines", { path: "a.ts" }),
createToolCall("2", "get_function", { path: "b.ts", name: "foo" }),
]
const xml = formatToolCallsAsXml(toolCalls)
expect(xml).toContain('<tool_call name="get_lines">')
expect(xml).toContain('<tool_call name="get_function">')
})
it("should handle object values as JSON", () => {
const toolCalls = [
createToolCall("1", "test", { data: { key: "value" } }),
]
const xml = formatToolCallsAsXml(toolCalls)
expect(xml).toContain('{"key":"value"}')
})
})
describe("extractThinking", () => {
it("should extract thinking content", () => {
const response = `<thinking>Let me analyze this.</thinking>
Here is the answer.`
const result = extractThinking(response)
expect(result.thinking).toBe("Let me analyze this.")
expect(result.content).toContain("Here is the answer.")
expect(result.content).not.toContain("thinking")
})
it("should handle multiple thinking blocks", () => {
const response = `<thinking>First thought.</thinking>
Some content.
<thinking>Second thought.</thinking>
More content.`
const result = extractThinking(response)
expect(result.thinking).toContain("First thought.")
expect(result.thinking).toContain("Second thought.")
})
it("should return original content if no thinking", () => {
const response = "Just a regular response."
const result = extractThinking(response)
expect(result.thinking).toBe("")
expect(result.content).toBe(response)
})
})
describe("hasToolCalls", () => {
it("should return true if response has tool calls", () => {
const response = `<tool_call name="get_lines"><path>a.ts</path></tool_call>`
expect(hasToolCalls(response)).toBe(true)
})
it("should return false if response has no tool calls", () => {
const response = "Just text without tool calls."
expect(hasToolCalls(response)).toBe(false)
})
})
describe("validateToolCallParams", () => {
it("should return valid for complete params", () => {
const params = { path: "src/index.ts", start: 1, end: 10 }
const required = ["path", "start", "end"]
const result = validateToolCallParams("get_lines", params, required)
expect(result.valid).toBe(true)
expect(result.errors).toHaveLength(0)
})
it("should return errors for missing required params", () => {
const params = { path: "src/index.ts" }
const required = ["path", "start", "end"]
const result = validateToolCallParams("get_lines", params, required)
expect(result.valid).toBe(false)
expect(result.errors).toHaveLength(2)
expect(result.errors).toContain("Missing required parameter: start")
expect(result.errors).toContain("Missing required parameter: end")
})
it("should treat null and undefined as missing", () => {
const params = { path: null, start: undefined }
const required = ["path", "start"]
const result = validateToolCallParams("test", params, required)
expect(result.valid).toBe(false)
expect(result.errors).toHaveLength(2)
})
it("should accept empty required array", () => {
const params = {}
const required: string[] = []
const result = validateToolCallParams("git_status", params, required)
expect(result.valid).toBe(true)
})
})
})

View File

@@ -0,0 +1,278 @@
import { describe, it, expect } from "vitest"
import {
SYSTEM_PROMPT,
buildInitialContext,
buildFileContext,
truncateContext,
type ProjectStructure,
} from "../../../../src/infrastructure/llm/prompts.js"
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
import type { FileMeta } from "../../../../src/domain/value-objects/FileMeta.js"
describe("prompts", () => {
describe("SYSTEM_PROMPT", () => {
it("should be a non-empty string", () => {
expect(typeof SYSTEM_PROMPT).toBe("string")
expect(SYSTEM_PROMPT.length).toBeGreaterThan(100)
})
it("should contain core principles", () => {
expect(SYSTEM_PROMPT).toContain("Lazy Loading")
expect(SYSTEM_PROMPT).toContain("Precision")
expect(SYSTEM_PROMPT).toContain("Safety")
})
it("should list available tools", () => {
expect(SYSTEM_PROMPT).toContain("get_lines")
expect(SYSTEM_PROMPT).toContain("edit_lines")
expect(SYSTEM_PROMPT).toContain("find_references")
expect(SYSTEM_PROMPT).toContain("git_status")
expect(SYSTEM_PROMPT).toContain("run_command")
})
it("should include safety rules", () => {
expect(SYSTEM_PROMPT).toContain("Safety Rules")
expect(SYSTEM_PROMPT).toContain("Never execute commands that could harm")
})
})
describe("buildInitialContext", () => {
const structure: ProjectStructure = {
name: "my-project",
rootPath: "/home/user/my-project",
files: ["src/index.ts", "src/utils.ts", "package.json"],
directories: ["src", "tests"],
}
const asts = new Map<string, FileAST>([
[
"src/index.ts",
{
imports: [],
exports: [],
functions: [
{
name: "main",
lineStart: 1,
lineEnd: 10,
params: [],
isAsync: false,
isExported: true,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
[
"src/utils.ts",
{
imports: [],
exports: [],
functions: [],
classes: [
{
name: "Helper",
lineStart: 1,
lineEnd: 20,
methods: [],
properties: [],
implements: [],
isExported: true,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
it("should include project header", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("# Project: my-project")
expect(context).toContain("Root: /home/user/my-project")
expect(context).toContain("Files: 3")
expect(context).toContain("Directories: 2")
})
it("should include directory structure", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("## Structure")
expect(context).toContain("src/")
expect(context).toContain("tests/")
})
it("should include file overview with AST summaries", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("## Files")
expect(context).toContain("src/index.ts")
expect(context).toContain("fn: main")
expect(context).toContain("src/utils.ts")
expect(context).toContain("class: Helper")
})
it("should include file flags from metadata", () => {
const metas = new Map<string, FileMeta>([
[
"src/index.ts",
{
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 75 },
dependencies: [],
dependents: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts", "f.ts"],
isHub: true,
isEntryPoint: true,
fileType: "source",
},
],
])
const context = buildInitialContext(structure, asts, metas)
expect(context).toContain("(hub, entry, complex)")
})
})
describe("buildFileContext", () => {
const ast: FileAST = {
imports: [
{ name: "fs", from: "node:fs", line: 1, type: "builtin", isDefault: false },
{ name: "helper", from: "./helper", line: 2, type: "internal", isDefault: true },
],
exports: [
{ name: "main", line: 10, isDefault: false, kind: "function" },
{ name: "Config", line: 20, isDefault: true, kind: "class" },
],
functions: [
{
name: "main",
lineStart: 10,
lineEnd: 30,
params: [
{ name: "args", optional: false, hasDefault: false },
{ name: "options", optional: true, hasDefault: false },
],
isAsync: true,
isExported: true,
},
],
classes: [
{
name: "Config",
lineStart: 40,
lineEnd: 80,
methods: [
{
name: "load",
lineStart: 50,
lineEnd: 60,
params: [],
isAsync: false,
visibility: "public",
isStatic: false,
},
],
properties: [],
extends: "BaseConfig",
implements: ["IConfig"],
isExported: true,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
}
it("should include file path header", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("## src/index.ts")
})
it("should include imports section", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Imports")
expect(context).toContain('fs from "node:fs" (builtin)')
expect(context).toContain('helper from "./helper" (internal)')
})
it("should include exports section", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Exports")
expect(context).toContain("function main")
expect(context).toContain("class Config (default)")
})
it("should include functions section", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Functions")
expect(context).toContain("async main(args, options)")
expect(context).toContain("[10-30]")
})
it("should include classes section with methods", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Classes")
expect(context).toContain("Config extends BaseConfig implements IConfig")
expect(context).toContain("[40-80]")
expect(context).toContain("load()")
})
it("should include metadata section when provided", () => {
const meta: FileMeta = {
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 65 },
dependencies: ["a.ts", "b.ts"],
dependents: ["c.ts"],
isHub: false,
isEntryPoint: true,
fileType: "source",
}
const context = buildFileContext("src/index.ts", ast, meta)
expect(context).toContain("### Metadata")
expect(context).toContain("LOC: 100")
expect(context).toContain("Complexity: 65/100")
expect(context).toContain("Dependencies: 2")
expect(context).toContain("Dependents: 1")
})
})
describe("truncateContext", () => {
it("should return original context if within limit", () => {
const context = "Short context"
const result = truncateContext(context, 1000)
expect(result).toBe(context)
})
it("should truncate long context", () => {
const context = "a".repeat(1000)
const result = truncateContext(context, 100)
expect(result.length).toBeLessThan(500)
expect(result).toContain("truncated")
})
it("should break at newline boundary", () => {
const context = "Line 1\nLine 2\nLine 3\n" + "a".repeat(1000)
const result = truncateContext(context, 50)
expect(result).toContain("truncated")
})
})
})

View File

@@ -0,0 +1,287 @@
import { describe, it, expect } from "vitest"
import {
ALL_TOOLS,
READ_TOOLS,
EDIT_TOOLS,
SEARCH_TOOLS,
ANALYSIS_TOOLS,
GIT_TOOLS,
RUN_TOOLS,
CONFIRMATION_TOOLS,
requiresConfirmation,
getToolDef,
getToolsByCategory,
GET_LINES_TOOL,
GET_FUNCTION_TOOL,
GET_CLASS_TOOL,
GET_STRUCTURE_TOOL,
EDIT_LINES_TOOL,
CREATE_FILE_TOOL,
DELETE_FILE_TOOL,
FIND_REFERENCES_TOOL,
FIND_DEFINITION_TOOL,
GET_DEPENDENCIES_TOOL,
GET_DEPENDENTS_TOOL,
GET_COMPLEXITY_TOOL,
GET_TODOS_TOOL,
GIT_STATUS_TOOL,
GIT_DIFF_TOOL,
GIT_COMMIT_TOOL,
RUN_COMMAND_TOOL,
RUN_TESTS_TOOL,
} from "../../../../src/infrastructure/llm/toolDefs.js"
describe("toolDefs", () => {
describe("ALL_TOOLS", () => {
it("should contain exactly 18 tools", () => {
expect(ALL_TOOLS).toHaveLength(18)
})
it("should have unique tool names", () => {
const names = ALL_TOOLS.map((t) => t.name)
const uniqueNames = new Set(names)
expect(uniqueNames.size).toBe(18)
})
it("should have valid structure for all tools", () => {
for (const tool of ALL_TOOLS) {
expect(tool.name).toBeDefined()
expect(typeof tool.name).toBe("string")
expect(tool.description).toBeDefined()
expect(typeof tool.description).toBe("string")
expect(Array.isArray(tool.parameters)).toBe(true)
}
})
})
describe("READ_TOOLS", () => {
it("should contain 4 read tools", () => {
expect(READ_TOOLS).toHaveLength(4)
})
it("should include all read tools", () => {
expect(READ_TOOLS).toContain(GET_LINES_TOOL)
expect(READ_TOOLS).toContain(GET_FUNCTION_TOOL)
expect(READ_TOOLS).toContain(GET_CLASS_TOOL)
expect(READ_TOOLS).toContain(GET_STRUCTURE_TOOL)
})
})
describe("EDIT_TOOLS", () => {
it("should contain 3 edit tools", () => {
expect(EDIT_TOOLS).toHaveLength(3)
})
it("should include all edit tools", () => {
expect(EDIT_TOOLS).toContain(EDIT_LINES_TOOL)
expect(EDIT_TOOLS).toContain(CREATE_FILE_TOOL)
expect(EDIT_TOOLS).toContain(DELETE_FILE_TOOL)
})
})
describe("SEARCH_TOOLS", () => {
it("should contain 2 search tools", () => {
expect(SEARCH_TOOLS).toHaveLength(2)
})
it("should include all search tools", () => {
expect(SEARCH_TOOLS).toContain(FIND_REFERENCES_TOOL)
expect(SEARCH_TOOLS).toContain(FIND_DEFINITION_TOOL)
})
})
describe("ANALYSIS_TOOLS", () => {
it("should contain 4 analysis tools", () => {
expect(ANALYSIS_TOOLS).toHaveLength(4)
})
it("should include all analysis tools", () => {
expect(ANALYSIS_TOOLS).toContain(GET_DEPENDENCIES_TOOL)
expect(ANALYSIS_TOOLS).toContain(GET_DEPENDENTS_TOOL)
expect(ANALYSIS_TOOLS).toContain(GET_COMPLEXITY_TOOL)
expect(ANALYSIS_TOOLS).toContain(GET_TODOS_TOOL)
})
})
describe("GIT_TOOLS", () => {
it("should contain 3 git tools", () => {
expect(GIT_TOOLS).toHaveLength(3)
})
it("should include all git tools", () => {
expect(GIT_TOOLS).toContain(GIT_STATUS_TOOL)
expect(GIT_TOOLS).toContain(GIT_DIFF_TOOL)
expect(GIT_TOOLS).toContain(GIT_COMMIT_TOOL)
})
})
describe("RUN_TOOLS", () => {
it("should contain 2 run tools", () => {
expect(RUN_TOOLS).toHaveLength(2)
})
it("should include all run tools", () => {
expect(RUN_TOOLS).toContain(RUN_COMMAND_TOOL)
expect(RUN_TOOLS).toContain(RUN_TESTS_TOOL)
})
})
describe("individual tool definitions", () => {
describe("GET_LINES_TOOL", () => {
it("should have correct name", () => {
expect(GET_LINES_TOOL.name).toBe("get_lines")
})
it("should have required path parameter", () => {
const pathParam = GET_LINES_TOOL.parameters.find((p) => p.name === "path")
expect(pathParam).toBeDefined()
expect(pathParam?.required).toBe(true)
})
it("should have optional start and end parameters", () => {
const startParam = GET_LINES_TOOL.parameters.find((p) => p.name === "start")
const endParam = GET_LINES_TOOL.parameters.find((p) => p.name === "end")
expect(startParam?.required).toBe(false)
expect(endParam?.required).toBe(false)
})
})
describe("EDIT_LINES_TOOL", () => {
it("should have all required parameters", () => {
const requiredParams = EDIT_LINES_TOOL.parameters.filter((p) => p.required)
const names = requiredParams.map((p) => p.name)
expect(names).toContain("path")
expect(names).toContain("start")
expect(names).toContain("end")
expect(names).toContain("content")
})
})
describe("GIT_STATUS_TOOL", () => {
it("should have no required parameters", () => {
expect(GIT_STATUS_TOOL.parameters).toHaveLength(0)
})
})
describe("GET_TODOS_TOOL", () => {
it("should have enum for type parameter", () => {
const typeParam = GET_TODOS_TOOL.parameters.find((p) => p.name === "type")
expect(typeParam?.enum).toEqual(["TODO", "FIXME", "HACK", "XXX"])
})
})
})
describe("CONFIRMATION_TOOLS", () => {
it("should be a Set", () => {
expect(CONFIRMATION_TOOLS instanceof Set).toBe(true)
})
it("should contain edit and git_commit tools", () => {
expect(CONFIRMATION_TOOLS.has("edit_lines")).toBe(true)
expect(CONFIRMATION_TOOLS.has("create_file")).toBe(true)
expect(CONFIRMATION_TOOLS.has("delete_file")).toBe(true)
expect(CONFIRMATION_TOOLS.has("git_commit")).toBe(true)
})
it("should not contain read tools", () => {
expect(CONFIRMATION_TOOLS.has("get_lines")).toBe(false)
expect(CONFIRMATION_TOOLS.has("get_function")).toBe(false)
})
})
describe("requiresConfirmation", () => {
it("should return true for edit tools", () => {
expect(requiresConfirmation("edit_lines")).toBe(true)
expect(requiresConfirmation("create_file")).toBe(true)
expect(requiresConfirmation("delete_file")).toBe(true)
})
it("should return true for git_commit", () => {
expect(requiresConfirmation("git_commit")).toBe(true)
})
it("should return false for read tools", () => {
expect(requiresConfirmation("get_lines")).toBe(false)
expect(requiresConfirmation("get_function")).toBe(false)
expect(requiresConfirmation("get_structure")).toBe(false)
})
it("should return false for analysis tools", () => {
expect(requiresConfirmation("get_dependencies")).toBe(false)
expect(requiresConfirmation("get_complexity")).toBe(false)
})
it("should return false for unknown tools", () => {
expect(requiresConfirmation("unknown_tool")).toBe(false)
})
})
describe("getToolDef", () => {
it("should return tool definition by name", () => {
const tool = getToolDef("get_lines")
expect(tool).toBe(GET_LINES_TOOL)
})
it("should return undefined for unknown tool", () => {
const tool = getToolDef("unknown_tool")
expect(tool).toBeUndefined()
})
it("should find all 18 tools", () => {
const names = [
"get_lines",
"get_function",
"get_class",
"get_structure",
"edit_lines",
"create_file",
"delete_file",
"find_references",
"find_definition",
"get_dependencies",
"get_dependents",
"get_complexity",
"get_todos",
"git_status",
"git_diff",
"git_commit",
"run_command",
"run_tests",
]
for (const name of names) {
expect(getToolDef(name)).toBeDefined()
}
})
})
describe("getToolsByCategory", () => {
it("should return read tools", () => {
expect(getToolsByCategory("read")).toBe(READ_TOOLS)
})
it("should return edit tools", () => {
expect(getToolsByCategory("edit")).toBe(EDIT_TOOLS)
})
it("should return search tools", () => {
expect(getToolsByCategory("search")).toBe(SEARCH_TOOLS)
})
it("should return analysis tools", () => {
expect(getToolsByCategory("analysis")).toBe(ANALYSIS_TOOLS)
})
it("should return git tools", () => {
expect(getToolsByCategory("git")).toBe(GIT_TOOLS)
})
it("should return run tools", () => {
expect(getToolsByCategory("run")).toBe(RUN_TOOLS)
})
it("should return empty array for unknown category", () => {
expect(getToolsByCategory("unknown")).toEqual([])
})
})
})