Files
puaros/packages/ipuaro/tests/unit/infrastructure/tools/search/FindDefinitionTool.test.ts
2025-12-01 02:05:27 +05:00

535 lines
21 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest"
import {
FindDefinitionTool,
type FindDefinitionResult,
} from "../../../../../src/infrastructure/tools/search/FindDefinitionTool.js"
import type { ToolContext } from "../../../../../src/domain/services/ITool.js"
import type {
IStorage,
SymbolIndex,
SymbolLocation,
} from "../../../../../src/domain/services/IStorage.js"
import type { FileData } from "../../../../../src/domain/value-objects/FileData.js"
function createMockFileData(lines: string[]): FileData {
return {
lines,
hash: "abc123",
size: lines.join("\n").length,
lastModified: Date.now(),
}
}
function createMockStorage(
files: Map<string, FileData> = new Map(),
symbolIndex: SymbolIndex = new Map(),
): IStorage {
return {
getFile: vi.fn().mockImplementation((p: string) => Promise.resolve(files.get(p) ?? null)),
setFile: vi.fn(),
deleteFile: vi.fn(),
getAllFiles: vi.fn().mockResolvedValue(files),
getFileCount: vi.fn().mockResolvedValue(files.size),
getAST: vi.fn().mockResolvedValue(null),
setAST: vi.fn(),
deleteAST: vi.fn(),
getAllASTs: vi.fn().mockResolvedValue(new Map()),
getMeta: vi.fn().mockResolvedValue(null),
setMeta: vi.fn(),
deleteMeta: vi.fn(),
getAllMetas: vi.fn().mockResolvedValue(new Map()),
getSymbolIndex: vi.fn().mockResolvedValue(symbolIndex),
setSymbolIndex: vi.fn(),
getDepsGraph: vi.fn().mockResolvedValue({ imports: new Map(), importedBy: new Map() }),
setDepsGraph: vi.fn(),
getProjectConfig: vi.fn(),
setProjectConfig: vi.fn(),
connect: vi.fn(),
disconnect: vi.fn(),
isConnected: vi.fn().mockReturnValue(true),
clear: vi.fn(),
} as unknown as IStorage
}
function createMockContext(storage?: IStorage): ToolContext {
return {
projectRoot: "/test/project",
storage: storage ?? createMockStorage(),
requestConfirmation: vi.fn().mockResolvedValue(true),
onProgress: vi.fn(),
}
}
describe("FindDefinitionTool", () => {
let tool: FindDefinitionTool
beforeEach(() => {
tool = new FindDefinitionTool()
})
describe("metadata", () => {
it("should have correct name", () => {
expect(tool.name).toBe("find_definition")
})
it("should have correct category", () => {
expect(tool.category).toBe("search")
})
it("should not require confirmation", () => {
expect(tool.requiresConfirmation).toBe(false)
})
it("should have correct parameters", () => {
expect(tool.parameters).toHaveLength(1)
expect(tool.parameters[0].name).toBe("symbol")
expect(tool.parameters[0].required).toBe(true)
})
it("should have description", () => {
expect(tool.description).toContain("Find where a symbol is defined")
})
})
describe("validateParams", () => {
it("should return null for valid params", () => {
expect(tool.validateParams({ symbol: "myFunction" })).toBeNull()
})
it("should return error for missing symbol", () => {
expect(tool.validateParams({})).toBe(
"Parameter 'symbol' is required and must be a non-empty string",
)
})
it("should return error for empty symbol", () => {
expect(tool.validateParams({ symbol: "" })).toBe(
"Parameter 'symbol' is required and must be a non-empty string",
)
})
it("should return error for whitespace-only symbol", () => {
expect(tool.validateParams({ symbol: " " })).toBe(
"Parameter 'symbol' is required and must be a non-empty string",
)
})
})
describe("execute", () => {
it("should find function definition", async () => {
const files = new Map<string, FileData>([
[
"src/utils.ts",
createMockFileData([
"// Utility functions",
"export function myFunction() {",
" return 42",
"}",
]),
],
])
const symbolIndex: SymbolIndex = new Map([
["myFunction", [{ path: "src/utils.ts", line: 2, type: "function" as const }]],
])
const storage = createMockStorage(files, symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "myFunction" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.symbol).toBe("myFunction")
expect(data.found).toBe(true)
expect(data.definitions).toHaveLength(1)
expect(data.definitions[0].path).toBe("src/utils.ts")
expect(data.definitions[0].line).toBe(2)
expect(data.definitions[0].type).toBe("function")
})
it("should find class definition", async () => {
const files = new Map<string, FileData>([
[
"src/models.ts",
createMockFileData([
"export class User {",
" constructor(public name: string) {}",
"}",
]),
],
])
const symbolIndex: SymbolIndex = new Map([
["User", [{ path: "src/models.ts", line: 1, type: "class" as const }]],
])
const storage = createMockStorage(files, symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "User" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(true)
expect(data.definitions[0].type).toBe("class")
})
it("should find interface definition", async () => {
const files = new Map<string, FileData>([
[
"src/types.ts",
createMockFileData(["export interface Config {", " port: number", "}"]),
],
])
const symbolIndex: SymbolIndex = new Map([
["Config", [{ path: "src/types.ts", line: 1, type: "interface" as const }]],
])
const storage = createMockStorage(files, symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "Config" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(true)
expect(data.definitions[0].type).toBe("interface")
})
it("should find type alias definition", async () => {
const symbolIndex: SymbolIndex = new Map([
["ID", [{ path: "src/types.ts", line: 1, type: "type" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "ID" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(true)
expect(data.definitions[0].type).toBe("type")
})
it("should find variable definition", async () => {
const symbolIndex: SymbolIndex = new Map([
["DEFAULT_CONFIG", [{ path: "src/config.ts", line: 5, type: "variable" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "DEFAULT_CONFIG" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(true)
expect(data.definitions[0].type).toBe("variable")
})
it("should find multiple definitions (function overloads)", async () => {
const symbolIndex: SymbolIndex = new Map([
[
"process",
[
{ path: "src/a.ts", line: 1, type: "function" as const },
{ path: "src/b.ts", line: 5, type: "function" as const },
],
],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "process" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(true)
expect(data.definitions).toHaveLength(2)
})
it("should return not found for unknown symbol", async () => {
const symbolIndex: SymbolIndex = new Map()
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "unknownSymbol" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(false)
expect(data.definitions).toHaveLength(0)
})
it("should suggest similar symbols when not found", async () => {
const symbolIndex: SymbolIndex = new Map([
["myFunction", [{ path: "src/a.ts", line: 1, type: "function" as const }]],
["myFunctionAsync", [{ path: "src/a.ts", line: 5, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "myFunc" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(false)
expect(data.suggestions).toBeDefined()
expect(data.suggestions).toContain("myFunction")
})
it("should not include suggestions when exact match found", async () => {
const symbolIndex: SymbolIndex = new Map([
["myFunction", [{ path: "src/a.ts", line: 1, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "myFunction" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(true)
expect(data.suggestions).toBeUndefined()
})
it("should include context lines", async () => {
const files = new Map<string, FileData>([
[
"src/test.ts",
createMockFileData([
"// Line 1",
"// Line 2",
"export function myFunc() {",
" return 1",
"}",
]),
],
])
const symbolIndex: SymbolIndex = new Map([
["myFunc", [{ path: "src/test.ts", line: 3, type: "function" as const }]],
])
const storage = createMockStorage(files, symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "myFunc" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
const context = data.definitions[0].context
expect(context).toContain("// Line 1")
expect(context).toContain("// Line 2")
expect(context).toContain("export function myFunc()")
expect(context).toContain("return 1")
expect(context).toContain("}")
})
it("should mark definition line in context", async () => {
const files = new Map<string, FileData>([
["src/test.ts", createMockFileData(["// before", "const foo = 1", "// after"])],
])
const symbolIndex: SymbolIndex = new Map([
["foo", [{ path: "src/test.ts", line: 2, type: "variable" as const }]],
])
const storage = createMockStorage(files, symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "foo" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
const context = data.definitions[0].context
expect(context).toContain("> 2│const foo = 1")
expect(context).toContain(" 1│// before")
})
it("should handle context at file start", async () => {
const files = new Map<string, FileData>([
["src/test.ts", createMockFileData(["const x = 1", "// after"])],
])
const symbolIndex: SymbolIndex = new Map([
["x", [{ path: "src/test.ts", line: 1, type: "variable" as const }]],
])
const storage = createMockStorage(files, symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "x" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
const context = data.definitions[0].context
expect(context).toContain("> 1│const x = 1")
})
it("should handle context at file end", async () => {
const files = new Map<string, FileData>([
["src/test.ts", createMockFileData(["// before", "const x = 1"])],
])
const symbolIndex: SymbolIndex = new Map([
["x", [{ path: "src/test.ts", line: 2, type: "variable" as const }]],
])
const storage = createMockStorage(files, symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "x" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
const context = data.definitions[0].context
expect(context).toContain("> 2│const x = 1")
})
it("should handle empty context when file not found", async () => {
const symbolIndex: SymbolIndex = new Map([
["foo", [{ path: "src/nonexistent.ts", line: 1, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "foo" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(true)
expect(data.definitions[0].context).toBe("")
})
it("should sort definitions by path then line", async () => {
const symbolIndex: SymbolIndex = new Map([
[
"foo",
[
{ path: "src/b.ts", line: 10, type: "function" as const },
{ path: "src/a.ts", line: 5, type: "function" as const },
{ path: "src/b.ts", line: 1, type: "function" as const },
],
],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "foo" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.definitions[0].path).toBe("src/a.ts")
expect(data.definitions[1].path).toBe("src/b.ts")
expect(data.definitions[1].line).toBe(1)
expect(data.definitions[2].path).toBe("src/b.ts")
expect(data.definitions[2].line).toBe(10)
})
it("should include callId in result", async () => {
const symbolIndex: SymbolIndex = new Map([
["x", [{ path: "src/a.ts", line: 1, type: "variable" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "x" }, ctx)
expect(result.callId).toMatch(/^find_definition-\d+$/)
})
it("should include execution time in result", async () => {
const symbolIndex: SymbolIndex = new Map()
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "x" }, ctx)
expect(result.executionTimeMs).toBeGreaterThanOrEqual(0)
})
it("should handle storage errors gracefully", async () => {
const storage = createMockStorage()
;(storage.getSymbolIndex as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Redis connection failed"),
)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "test" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Redis connection failed")
})
it("should trim symbol before searching", async () => {
const symbolIndex: SymbolIndex = new Map([
["foo", [{ path: "src/a.ts", line: 1, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: " foo " }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.symbol).toBe("foo")
expect(data.found).toBe(true)
})
it("should suggest symbols with small edit distance", async () => {
const symbolIndex: SymbolIndex = new Map([
["fetchData", [{ path: "src/a.ts", line: 1, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "fethcData" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(false)
expect(data.suggestions).toContain("fetchData")
})
it("should limit suggestions to 5", async () => {
const symbolIndex: SymbolIndex = new Map([
["testA", [{ path: "a.ts", line: 1, type: "function" as const }]],
["testB", [{ path: "b.ts", line: 1, type: "function" as const }]],
["testC", [{ path: "c.ts", line: 1, type: "function" as const }]],
["testD", [{ path: "d.ts", line: 1, type: "function" as const }]],
["testE", [{ path: "e.ts", line: 1, type: "function" as const }]],
["testF", [{ path: "f.ts", line: 1, type: "function" as const }]],
["testG", [{ path: "g.ts", line: 1, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "test" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.suggestions).toBeDefined()
expect(data.suggestions!.length).toBeLessThanOrEqual(5)
})
it("should sort suggestions alphabetically", async () => {
const symbolIndex: SymbolIndex = new Map([
["testC", [{ path: "c.ts", line: 1, type: "function" as const }]],
["testA", [{ path: "a.ts", line: 1, type: "function" as const }]],
["testB", [{ path: "b.ts", line: 1, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "test" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.suggestions).toEqual(["testA", "testB", "testC"])
})
it("should not include suggestions when no similar symbols exist", async () => {
const symbolIndex: SymbolIndex = new Map([
["xyz", [{ path: "a.ts", line: 1, type: "function" as const }]],
])
const storage = createMockStorage(new Map(), symbolIndex)
const ctx = createMockContext(storage)
const result = await tool.execute({ symbol: "abc" }, ctx)
expect(result.success).toBe(true)
const data = result.data as FindDefinitionResult
expect(data.found).toBe(false)
expect(data.suggestions).toBeUndefined()
})
})
})