feat(ipuaro): add analysis tools (v0.8.0)

- GetDependenciesTool: get files a file imports
- GetDependentsTool: get files that import a file
- GetComplexityTool: get complexity metrics
- GetTodosTool: find TODO/FIXME/HACK comments

Tests: 853 (+120), Coverage: 97.91%
This commit is contained in:
imfozilbek
2025-12-01 02:23:36 +05:00
parent caf7aac116
commit 2ae1ac13f5
12 changed files with 2669 additions and 1 deletions

View File

@@ -0,0 +1,513 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import {
GetComplexityTool,
type GetComplexityResult,
} from "../../../../../src/infrastructure/tools/analysis/GetComplexityTool.js"
import type { ToolContext } from "../../../../../src/domain/services/ITool.js"
import type { IStorage } from "../../../../../src/domain/services/IStorage.js"
import type { FileMeta } from "../../../../../src/domain/value-objects/FileMeta.js"
function createMockFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
return {
complexity: { loc: 10, nesting: 2, cyclomaticComplexity: 5, score: 25 },
dependencies: [],
dependents: [],
isHub: false,
isEntryPoint: false,
fileType: "source",
...partial,
}
}
function createMockStorage(metas: Map<string, FileMeta> = new Map()): IStorage {
return {
getFile: vi.fn().mockResolvedValue(null),
setFile: vi.fn(),
deleteFile: vi.fn(),
getAllFiles: vi.fn().mockResolvedValue(new Map()),
getFileCount: vi.fn().mockResolvedValue(0),
getAST: vi.fn().mockResolvedValue(null),
setAST: vi.fn(),
deleteAST: vi.fn(),
getAllASTs: vi.fn().mockResolvedValue(new Map()),
getMeta: vi.fn().mockImplementation((p: string) => Promise.resolve(metas.get(p) ?? null)),
setMeta: vi.fn(),
deleteMeta: vi.fn(),
getAllMetas: vi.fn().mockResolvedValue(metas),
getSymbolIndex: vi.fn().mockResolvedValue(new Map()),
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("GetComplexityTool", () => {
let tool: GetComplexityTool
beforeEach(() => {
tool = new GetComplexityTool()
})
describe("metadata", () => {
it("should have correct name", () => {
expect(tool.name).toBe("get_complexity")
})
it("should have correct category", () => {
expect(tool.category).toBe("analysis")
})
it("should not require confirmation", () => {
expect(tool.requiresConfirmation).toBe(false)
})
it("should have correct parameters", () => {
expect(tool.parameters).toHaveLength(2)
expect(tool.parameters[0].name).toBe("path")
expect(tool.parameters[0].required).toBe(false)
expect(tool.parameters[1].name).toBe("limit")
expect(tool.parameters[1].required).toBe(false)
})
it("should have description", () => {
expect(tool.description).toContain("complexity")
})
})
describe("validateParams", () => {
it("should return null for no params", () => {
expect(tool.validateParams({})).toBeNull()
})
it("should return null for valid path", () => {
expect(tool.validateParams({ path: "src/index.ts" })).toBeNull()
})
it("should return null for valid limit", () => {
expect(tool.validateParams({ limit: 10 })).toBeNull()
})
it("should return null for valid path and limit", () => {
expect(tool.validateParams({ path: "src", limit: 5 })).toBeNull()
})
it("should return error for non-string path", () => {
expect(tool.validateParams({ path: 123 })).toBe("Parameter 'path' must be a string")
})
it("should return error for non-integer limit", () => {
expect(tool.validateParams({ limit: 10.5 })).toBe(
"Parameter 'limit' must be an integer",
)
})
it("should return error for non-number limit", () => {
expect(tool.validateParams({ limit: "10" })).toBe(
"Parameter 'limit' must be an integer",
)
})
it("should return error for limit less than 1", () => {
expect(tool.validateParams({ limit: 0 })).toBe("Parameter 'limit' must be at least 1")
})
it("should return error for negative limit", () => {
expect(tool.validateParams({ limit: -5 })).toBe("Parameter 'limit' must be at least 1")
})
})
describe("execute", () => {
it("should return complexity for all files without path", async () => {
const metas = new Map<string, FileMeta>([
[
"src/a.ts",
createMockFileMeta({
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 50 },
}),
],
[
"src/b.ts",
createMockFileMeta({
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 25 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.analyzedPath).toBeNull()
expect(data.totalFiles).toBe(2)
expect(data.files).toHaveLength(2)
})
it("should sort files by complexity score descending", async () => {
const metas = new Map<string, FileMeta>([
[
"src/low.ts",
createMockFileMeta({
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 2, score: 10 },
}),
],
[
"src/high.ts",
createMockFileMeta({
complexity: { loc: 200, nesting: 5, cyclomaticComplexity: 25, score: 80 },
}),
],
[
"src/mid.ts",
createMockFileMeta({
complexity: { loc: 50, nesting: 3, cyclomaticComplexity: 10, score: 40 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.files[0].path).toBe("src/high.ts")
expect(data.files[1].path).toBe("src/mid.ts")
expect(data.files[2].path).toBe("src/low.ts")
})
it("should filter by path prefix", async () => {
const metas = new Map<string, FileMeta>([
["src/a.ts", createMockFileMeta()],
["src/b.ts", createMockFileMeta()],
["lib/c.ts", createMockFileMeta()],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.analyzedPath).toBe("src")
expect(data.totalFiles).toBe(2)
expect(data.files.every((f) => f.path.startsWith("src/"))).toBe(true)
})
it("should filter by specific file path", async () => {
const metas = new Map<string, FileMeta>([
[
"src/a.ts",
createMockFileMeta({
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 15, score: 55 },
}),
],
["src/b.ts", createMockFileMeta()],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/a.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.totalFiles).toBe(1)
expect(data.files[0].path).toBe("src/a.ts")
expect(data.files[0].metrics.score).toBe(55)
})
it("should respect limit parameter", async () => {
const metas = new Map<string, FileMeta>([
[
"src/a.ts",
createMockFileMeta({
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 70 },
}),
],
[
"src/b.ts",
createMockFileMeta({
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 50 },
}),
],
[
"src/c.ts",
createMockFileMeta({
complexity: { loc: 20, nesting: 1, cyclomaticComplexity: 2, score: 20 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ limit: 2 }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.totalFiles).toBe(3)
expect(data.files).toHaveLength(2)
expect(data.files[0].metrics.score).toBe(70)
expect(data.files[1].metrics.score).toBe(50)
})
it("should use default limit of 20", async () => {
const metas = new Map<string, FileMeta>()
for (let i = 0; i < 30; i++) {
metas.set(`src/file${String(i)}.ts`, createMockFileMeta())
}
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.totalFiles).toBe(30)
expect(data.files).toHaveLength(20)
})
it("should calculate average score", async () => {
const metas = new Map<string, FileMeta>([
[
"src/a.ts",
createMockFileMeta({
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 60 },
}),
],
[
"src/b.ts",
createMockFileMeta({
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 40 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.averageScore).toBe(50)
})
it("should calculate summary statistics", async () => {
const metas = new Map<string, FileMeta>([
[
"src/high.ts",
createMockFileMeta({
complexity: { loc: 200, nesting: 5, cyclomaticComplexity: 25, score: 75 },
}),
],
[
"src/medium.ts",
createMockFileMeta({
complexity: { loc: 80, nesting: 3, cyclomaticComplexity: 12, score: 45 },
}),
],
[
"src/low.ts",
createMockFileMeta({
complexity: { loc: 20, nesting: 1, cyclomaticComplexity: 3, score: 15 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.summary.highComplexity).toBe(1)
expect(data.summary.mediumComplexity).toBe(1)
expect(data.summary.lowComplexity).toBe(1)
})
it("should return empty result for empty project", async () => {
const storage = createMockStorage()
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.totalFiles).toBe(0)
expect(data.averageScore).toBe(0)
expect(data.files).toEqual([])
expect(data.summary).toEqual({
highComplexity: 0,
mediumComplexity: 0,
lowComplexity: 0,
})
})
it("should return error for non-existent path", async () => {
const metas = new Map<string, FileMeta>([["src/a.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "nonexistent" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toContain("No files found at path")
})
it("should handle absolute paths", async () => {
const metas = new Map<string, FileMeta>([
["src/a.ts", createMockFileMeta()],
["src/b.ts", createMockFileMeta()],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "/test/project/src" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.analyzedPath).toBe("src")
expect(data.totalFiles).toBe(2)
})
it("should include file metadata", async () => {
const metas = new Map<string, FileMeta>([
[
"src/hub.ts",
createMockFileMeta({
fileType: "source",
isHub: true,
complexity: { loc: 150, nesting: 4, cyclomaticComplexity: 18, score: 65 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.files[0].fileType).toBe("source")
expect(data.files[0].isHub).toBe(true)
expect(data.files[0].metrics).toEqual({
loc: 150,
nesting: 4,
cyclomaticComplexity: 18,
score: 65,
})
})
it("should include callId in result", async () => {
const metas = new Map<string, FileMeta>([["src/a.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.callId).toMatch(/^get_complexity-\d+$/)
})
it("should include execution time in result", async () => {
const metas = new Map<string, FileMeta>([["src/a.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.executionTimeMs).toBeGreaterThanOrEqual(0)
})
it("should handle storage errors gracefully", async () => {
const storage = createMockStorage()
;(storage.getAllMetas as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Redis connection failed"),
)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Redis connection failed")
})
it("should round average score to 2 decimal places", async () => {
const metas = new Map<string, FileMeta>([
[
"src/a.ts",
createMockFileMeta({
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 33 },
}),
],
[
"src/b.ts",
createMockFileMeta({
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 33 },
}),
],
[
"src/c.ts",
createMockFileMeta({
complexity: { loc: 20, nesting: 1, cyclomaticComplexity: 2, score: 34 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.averageScore).toBe(33.33)
})
it("should handle complexity threshold boundaries", async () => {
const metas = new Map<string, FileMeta>([
[
"src/exact-high.ts",
createMockFileMeta({
complexity: { loc: 200, nesting: 5, cyclomaticComplexity: 20, score: 60 },
}),
],
[
"src/exact-medium.ts",
createMockFileMeta({
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 30 },
}),
],
[
"src/below-medium.ts",
createMockFileMeta({
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 29 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetComplexityResult
expect(data.summary.highComplexity).toBe(1)
expect(data.summary.mediumComplexity).toBe(1)
expect(data.summary.lowComplexity).toBe(1)
})
})
})

View File

@@ -0,0 +1,342 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import {
GetDependenciesTool,
type GetDependenciesResult,
} from "../../../../../src/infrastructure/tools/analysis/GetDependenciesTool.js"
import type { ToolContext } from "../../../../../src/domain/services/ITool.js"
import type { IStorage } from "../../../../../src/domain/services/IStorage.js"
import type { FileMeta } from "../../../../../src/domain/value-objects/FileMeta.js"
function createMockFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
return {
complexity: { loc: 10, nesting: 2, cyclomaticComplexity: 5, score: 25 },
dependencies: [],
dependents: [],
isHub: false,
isEntryPoint: false,
fileType: "source",
...partial,
}
}
function createMockStorage(metas: Map<string, FileMeta> = new Map()): IStorage {
return {
getFile: vi.fn().mockResolvedValue(null),
setFile: vi.fn(),
deleteFile: vi.fn(),
getAllFiles: vi.fn().mockResolvedValue(new Map()),
getFileCount: vi.fn().mockResolvedValue(0),
getAST: vi.fn().mockResolvedValue(null),
setAST: vi.fn(),
deleteAST: vi.fn(),
getAllASTs: vi.fn().mockResolvedValue(new Map()),
getMeta: vi.fn().mockImplementation((p: string) => Promise.resolve(metas.get(p) ?? null)),
setMeta: vi.fn(),
deleteMeta: vi.fn(),
getAllMetas: vi.fn().mockResolvedValue(metas),
getSymbolIndex: vi.fn().mockResolvedValue(new Map()),
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("GetDependenciesTool", () => {
let tool: GetDependenciesTool
beforeEach(() => {
tool = new GetDependenciesTool()
})
describe("metadata", () => {
it("should have correct name", () => {
expect(tool.name).toBe("get_dependencies")
})
it("should have correct category", () => {
expect(tool.category).toBe("analysis")
})
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("path")
expect(tool.parameters[0].required).toBe(true)
})
it("should have description", () => {
expect(tool.description).toContain("imports")
})
})
describe("validateParams", () => {
it("should return null for valid path", () => {
expect(tool.validateParams({ path: "src/index.ts" })).toBeNull()
})
it("should return error for missing path", () => {
expect(tool.validateParams({})).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
it("should return error for empty path", () => {
expect(tool.validateParams({ path: "" })).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
it("should return error for whitespace-only path", () => {
expect(tool.validateParams({ path: " " })).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
it("should return error for non-string path", () => {
expect(tool.validateParams({ path: 123 })).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
})
describe("execute", () => {
it("should return dependencies for a file", async () => {
const metas = new Map<string, FileMeta>([
[
"src/index.ts",
createMockFileMeta({
dependencies: ["src/utils.ts", "src/config.ts"],
}),
],
["src/utils.ts", createMockFileMeta({ isHub: true })],
["src/config.ts", createMockFileMeta({ isEntryPoint: true })],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.file).toBe("src/index.ts")
expect(data.totalDependencies).toBe(2)
expect(data.dependencies).toHaveLength(2)
})
it("should include metadata for each dependency", async () => {
const metas = new Map<string, FileMeta>([
[
"src/index.ts",
createMockFileMeta({
dependencies: ["src/utils.ts"],
}),
],
[
"src/utils.ts",
createMockFileMeta({
isHub: true,
isEntryPoint: false,
fileType: "source",
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.dependencies[0]).toEqual({
path: "src/utils.ts",
exists: true,
isEntryPoint: false,
isHub: true,
fileType: "source",
})
})
it("should handle file with no dependencies", async () => {
const metas = new Map<string, FileMeta>([
["src/standalone.ts", createMockFileMeta({ dependencies: [] })],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/standalone.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.totalDependencies).toBe(0)
expect(data.dependencies).toEqual([])
})
it("should return error for non-existent file", async () => {
const storage = createMockStorage()
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "nonexistent.ts" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toContain("File not found or not indexed")
})
it("should handle absolute paths", async () => {
const metas = new Map<string, FileMeta>([
["src/index.ts", createMockFileMeta({ dependencies: [] })],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "/test/project/src/index.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.file).toBe("src/index.ts")
})
it("should mark non-existent dependencies", async () => {
const metas = new Map<string, FileMeta>([
[
"src/index.ts",
createMockFileMeta({
dependencies: ["src/missing.ts"],
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.dependencies[0].exists).toBe(false)
expect(data.dependencies[0].isHub).toBe(false)
expect(data.dependencies[0].fileType).toBe("unknown")
})
it("should sort dependencies by path", async () => {
const metas = new Map<string, FileMeta>([
[
"src/index.ts",
createMockFileMeta({
dependencies: ["src/z.ts", "src/a.ts", "src/m.ts"],
}),
],
["src/z.ts", createMockFileMeta()],
["src/a.ts", createMockFileMeta()],
["src/m.ts", createMockFileMeta()],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.dependencies[0].path).toBe("src/a.ts")
expect(data.dependencies[1].path).toBe("src/m.ts")
expect(data.dependencies[2].path).toBe("src/z.ts")
})
it("should include file type of source file", async () => {
const metas = new Map<string, FileMeta>([
[
"tests/index.test.ts",
createMockFileMeta({
fileType: "test",
dependencies: [],
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "tests/index.test.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.fileType).toBe("test")
})
it("should include callId in result", async () => {
const metas = new Map<string, FileMeta>([["src/index.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.callId).toMatch(/^get_dependencies-\d+$/)
})
it("should include execution time in result", async () => {
const metas = new Map<string, FileMeta>([["src/index.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.executionTimeMs).toBeGreaterThanOrEqual(0)
})
it("should handle storage errors gracefully", async () => {
const storage = createMockStorage()
;(storage.getMeta as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Redis connection failed"),
)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Redis connection failed")
})
it("should trim path before searching", async () => {
const metas = new Map<string, FileMeta>([["src/index.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: " src/index.ts " }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.file).toBe("src/index.ts")
})
it("should handle many dependencies", async () => {
const deps = Array.from({ length: 50 }, (_, i) => `src/dep${String(i)}.ts`)
const metas = new Map<string, FileMeta>([
["src/index.ts", createMockFileMeta({ dependencies: deps })],
...deps.map((dep) => [dep, createMockFileMeta()] as [string, FileMeta]),
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/index.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependenciesResult
expect(data.totalDependencies).toBe(50)
})
})
})

View File

@@ -0,0 +1,388 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import {
GetDependentsTool,
type GetDependentsResult,
} from "../../../../../src/infrastructure/tools/analysis/GetDependentsTool.js"
import type { ToolContext } from "../../../../../src/domain/services/ITool.js"
import type { IStorage } from "../../../../../src/domain/services/IStorage.js"
import type { FileMeta } from "../../../../../src/domain/value-objects/FileMeta.js"
function createMockFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
return {
complexity: { loc: 10, nesting: 2, cyclomaticComplexity: 5, score: 25 },
dependencies: [],
dependents: [],
isHub: false,
isEntryPoint: false,
fileType: "source",
...partial,
}
}
function createMockStorage(metas: Map<string, FileMeta> = new Map()): IStorage {
return {
getFile: vi.fn().mockResolvedValue(null),
setFile: vi.fn(),
deleteFile: vi.fn(),
getAllFiles: vi.fn().mockResolvedValue(new Map()),
getFileCount: vi.fn().mockResolvedValue(0),
getAST: vi.fn().mockResolvedValue(null),
setAST: vi.fn(),
deleteAST: vi.fn(),
getAllASTs: vi.fn().mockResolvedValue(new Map()),
getMeta: vi.fn().mockImplementation((p: string) => Promise.resolve(metas.get(p) ?? null)),
setMeta: vi.fn(),
deleteMeta: vi.fn(),
getAllMetas: vi.fn().mockResolvedValue(metas),
getSymbolIndex: vi.fn().mockResolvedValue(new Map()),
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("GetDependentsTool", () => {
let tool: GetDependentsTool
beforeEach(() => {
tool = new GetDependentsTool()
})
describe("metadata", () => {
it("should have correct name", () => {
expect(tool.name).toBe("get_dependents")
})
it("should have correct category", () => {
expect(tool.category).toBe("analysis")
})
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("path")
expect(tool.parameters[0].required).toBe(true)
})
it("should have description", () => {
expect(tool.description).toContain("import")
})
})
describe("validateParams", () => {
it("should return null for valid path", () => {
expect(tool.validateParams({ path: "src/utils.ts" })).toBeNull()
})
it("should return error for missing path", () => {
expect(tool.validateParams({})).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
it("should return error for empty path", () => {
expect(tool.validateParams({ path: "" })).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
it("should return error for whitespace-only path", () => {
expect(tool.validateParams({ path: " " })).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
it("should return error for non-string path", () => {
expect(tool.validateParams({ path: 123 })).toBe(
"Parameter 'path' is required and must be a non-empty string",
)
})
})
describe("execute", () => {
it("should return dependents for a file", async () => {
const metas = new Map<string, FileMeta>([
[
"src/utils.ts",
createMockFileMeta({
dependents: ["src/index.ts", "src/app.ts"],
isHub: true,
}),
],
["src/index.ts", createMockFileMeta({ isEntryPoint: true })],
["src/app.ts", createMockFileMeta()],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.file).toBe("src/utils.ts")
expect(data.totalDependents).toBe(2)
expect(data.isHub).toBe(true)
expect(data.dependents).toHaveLength(2)
})
it("should include metadata for each dependent", async () => {
const metas = new Map<string, FileMeta>([
[
"src/utils.ts",
createMockFileMeta({
dependents: ["src/index.ts"],
}),
],
[
"src/index.ts",
createMockFileMeta({
isHub: false,
isEntryPoint: true,
fileType: "source",
complexity: { loc: 50, nesting: 3, cyclomaticComplexity: 10, score: 45 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.dependents[0]).toEqual({
path: "src/index.ts",
isEntryPoint: true,
isHub: false,
fileType: "source",
complexityScore: 45,
})
})
it("should handle file with no dependents", async () => {
const metas = new Map<string, FileMeta>([
["src/isolated.ts", createMockFileMeta({ dependents: [] })],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/isolated.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.totalDependents).toBe(0)
expect(data.isHub).toBe(false)
expect(data.dependents).toEqual([])
})
it("should return error for non-existent file", async () => {
const storage = createMockStorage()
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "nonexistent.ts" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toContain("File not found or not indexed")
})
it("should handle absolute paths", async () => {
const metas = new Map<string, FileMeta>([
["src/utils.ts", createMockFileMeta({ dependents: [] })],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "/test/project/src/utils.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.file).toBe("src/utils.ts")
})
it("should handle missing dependent metadata", async () => {
const metas = new Map<string, FileMeta>([
[
"src/utils.ts",
createMockFileMeta({
dependents: ["src/missing.ts"],
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.dependents[0].isHub).toBe(false)
expect(data.dependents[0].isEntryPoint).toBe(false)
expect(data.dependents[0].fileType).toBe("unknown")
expect(data.dependents[0].complexityScore).toBe(0)
})
it("should sort dependents by path", async () => {
const metas = new Map<string, FileMeta>([
[
"src/utils.ts",
createMockFileMeta({
dependents: ["src/z.ts", "src/a.ts", "src/m.ts"],
}),
],
["src/z.ts", createMockFileMeta()],
["src/a.ts", createMockFileMeta()],
["src/m.ts", createMockFileMeta()],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.dependents[0].path).toBe("src/a.ts")
expect(data.dependents[1].path).toBe("src/m.ts")
expect(data.dependents[2].path).toBe("src/z.ts")
})
it("should include file type of source file", async () => {
const metas = new Map<string, FileMeta>([
[
"src/types.ts",
createMockFileMeta({
fileType: "types",
dependents: [],
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/types.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.fileType).toBe("types")
})
it("should correctly identify hub files", async () => {
const dependents = Array.from({ length: 10 }, (_, i) => `src/file${String(i)}.ts`)
const metas = new Map<string, FileMeta>([
[
"src/core.ts",
createMockFileMeta({
dependents,
isHub: true,
}),
],
...dependents.map((dep) => [dep, createMockFileMeta()] as [string, FileMeta]),
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/core.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.isHub).toBe(true)
expect(data.totalDependents).toBe(10)
})
it("should include callId in result", async () => {
const metas = new Map<string, FileMeta>([["src/utils.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.callId).toMatch(/^get_dependents-\d+$/)
})
it("should include execution time in result", async () => {
const metas = new Map<string, FileMeta>([["src/utils.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.executionTimeMs).toBeGreaterThanOrEqual(0)
})
it("should handle storage errors gracefully", async () => {
const storage = createMockStorage()
;(storage.getMeta as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Redis connection failed"),
)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Redis connection failed")
})
it("should trim path before searching", async () => {
const metas = new Map<string, FileMeta>([["src/utils.ts", createMockFileMeta()]])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: " src/utils.ts " }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
expect(data.file).toBe("src/utils.ts")
})
it("should include complexity scores for dependents", async () => {
const metas = new Map<string, FileMeta>([
[
"src/utils.ts",
createMockFileMeta({
dependents: ["src/high.ts", "src/low.ts"],
}),
],
[
"src/high.ts",
createMockFileMeta({
complexity: { loc: 200, nesting: 5, cyclomaticComplexity: 20, score: 80 },
}),
],
[
"src/low.ts",
createMockFileMeta({
complexity: { loc: 20, nesting: 1, cyclomaticComplexity: 2, score: 10 },
}),
],
])
const storage = createMockStorage(metas)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/utils.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetDependentsResult
const highDep = data.dependents.find((d) => d.path === "src/high.ts")
const lowDep = data.dependents.find((d) => d.path === "src/low.ts")
expect(highDep?.complexityScore).toBe(80)
expect(lowDep?.complexityScore).toBe(10)
})
})
})

View File

@@ -0,0 +1,583 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import {
GetTodosTool,
type GetTodosResult,
} from "../../../../../src/infrastructure/tools/analysis/GetTodosTool.js"
import type { ToolContext } from "../../../../../src/domain/services/ITool.js"
import type { IStorage } 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()): 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(new Map()),
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("GetTodosTool", () => {
let tool: GetTodosTool
beforeEach(() => {
tool = new GetTodosTool()
})
describe("metadata", () => {
it("should have correct name", () => {
expect(tool.name).toBe("get_todos")
})
it("should have correct category", () => {
expect(tool.category).toBe("analysis")
})
it("should not require confirmation", () => {
expect(tool.requiresConfirmation).toBe(false)
})
it("should have correct parameters", () => {
expect(tool.parameters).toHaveLength(2)
expect(tool.parameters[0].name).toBe("path")
expect(tool.parameters[0].required).toBe(false)
expect(tool.parameters[1].name).toBe("type")
expect(tool.parameters[1].required).toBe(false)
})
it("should have description", () => {
expect(tool.description).toContain("TODO")
expect(tool.description).toContain("FIXME")
})
})
describe("validateParams", () => {
it("should return null for no params", () => {
expect(tool.validateParams({})).toBeNull()
})
it("should return null for valid path", () => {
expect(tool.validateParams({ path: "src" })).toBeNull()
})
it("should return null for valid type", () => {
expect(tool.validateParams({ type: "TODO" })).toBeNull()
})
it("should return null for lowercase type", () => {
expect(tool.validateParams({ type: "fixme" })).toBeNull()
})
it("should return null for path and type", () => {
expect(tool.validateParams({ path: "src", type: "TODO" })).toBeNull()
})
it("should return error for non-string path", () => {
expect(tool.validateParams({ path: 123 })).toBe("Parameter 'path' must be a string")
})
it("should return error for non-string type", () => {
expect(tool.validateParams({ type: 123 })).toBe("Parameter 'type' must be a string")
})
it("should return error for invalid type", () => {
expect(tool.validateParams({ type: "INVALID" })).toBe(
"Parameter 'type' must be one of: TODO, FIXME, HACK, XXX, BUG, NOTE",
)
})
})
describe("execute", () => {
it("should find TODO comments", async () => {
const files = new Map<string, FileData>([
[
"src/index.ts",
createMockFileData([
"// TODO: implement this",
"function foo() {}",
"// TODO: add tests",
]),
],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(2)
expect(data.todos[0].type).toBe("TODO")
expect(data.todos[0].text).toBe("implement this")
expect(data.todos[1].text).toBe("add tests")
})
it("should find FIXME comments", async () => {
const files = new Map<string, FileData>([
[
"src/index.ts",
createMockFileData(["// FIXME: broken logic here", "const x = 1"]),
],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(1)
expect(data.todos[0].type).toBe("FIXME")
expect(data.todos[0].text).toBe("broken logic here")
})
it("should find HACK comments", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// HACK: temporary workaround"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].type).toBe("HACK")
})
it("should find XXX comments", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// XXX: needs attention"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].type).toBe("XXX")
})
it("should find BUG comments", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// BUG: race condition"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].type).toBe("BUG")
})
it("should find NOTE comments", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// NOTE: important consideration"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].type).toBe("NOTE")
})
it("should find comments in block comments", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["/*", " * TODO: in block comment", " */"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(1)
expect(data.todos[0].text).toBe("in block comment")
})
it("should find comments with author annotation", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// TODO(john): fix this"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].text).toBe("fix this")
})
it("should handle TODO without colon", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// TODO implement feature"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].text).toBe("implement feature")
})
it("should filter by type", async () => {
const files = new Map<string, FileData>([
[
"src/index.ts",
createMockFileData([
"// TODO: task one",
"// FIXME: bug here",
"// TODO: task two",
]),
],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({ type: "TODO" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(2)
expect(data.todos.every((t) => t.type === "TODO")).toBe(true)
})
it("should filter by type case-insensitively", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// TODO: task", "// FIXME: bug"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({ type: "todo" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(1)
expect(data.todos[0].type).toBe("TODO")
})
it("should filter by path", async () => {
const files = new Map<string, FileData>([
["src/a.ts", createMockFileData(["// TODO: in src"])],
["lib/b.ts", createMockFileData(["// TODO: in lib"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.searchedPath).toBe("src")
expect(data.totalTodos).toBe(1)
expect(data.todos[0].path).toBe("src/a.ts")
})
it("should filter by specific file", async () => {
const files = new Map<string, FileData>([
["src/a.ts", createMockFileData(["// TODO: in a"])],
["src/b.ts", createMockFileData(["// TODO: in b"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "src/a.ts" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(1)
expect(data.todos[0].path).toBe("src/a.ts")
})
it("should return error for non-existent path", async () => {
const files = new Map<string, FileData>([
["src/a.ts", createMockFileData(["// TODO: task"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "nonexistent" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toContain("No files found at path")
})
it("should count by type", async () => {
const files = new Map<string, FileData>([
[
"src/index.ts",
createMockFileData([
"// TODO: task 1",
"// TODO: task 2",
"// FIXME: bug",
"// HACK: workaround",
]),
],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.byType.TODO).toBe(2)
expect(data.byType.FIXME).toBe(1)
expect(data.byType.HACK).toBe(1)
expect(data.byType.XXX).toBe(0)
expect(data.byType.BUG).toBe(0)
expect(data.byType.NOTE).toBe(0)
})
it("should count files with todos", async () => {
const files = new Map<string, FileData>([
["src/a.ts", createMockFileData(["// TODO: task"])],
["src/b.ts", createMockFileData(["const x = 1"])],
["src/c.ts", createMockFileData(["// TODO: another task"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.filesWithTodos).toBe(2)
})
it("should sort results by path then line", async () => {
const files = new Map<string, FileData>([
["src/b.ts", createMockFileData(["// TODO: b1", "", "// TODO: b2"])],
["src/a.ts", createMockFileData(["// TODO: a1"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].path).toBe("src/a.ts")
expect(data.todos[1].path).toBe("src/b.ts")
expect(data.todos[1].line).toBe(1)
expect(data.todos[2].path).toBe("src/b.ts")
expect(data.todos[2].line).toBe(3)
})
it("should include line context", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData([" // TODO: indented task"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].context).toBe("// TODO: indented task")
})
it("should return empty result for empty project", async () => {
const storage = createMockStorage()
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(0)
expect(data.filesWithTodos).toBe(0)
expect(data.todos).toEqual([])
})
it("should return empty result when no todos found", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["const x = 1", "const y = 2"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(0)
})
it("should handle TODO without description", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// TODO:"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].text).toBe("(no description)")
})
it("should handle absolute paths", async () => {
const files = new Map<string, FileData>([
["src/a.ts", createMockFileData(["// TODO: task"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({ path: "/test/project/src" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.searchedPath).toBe("src")
})
it("should find todos with hash comments", async () => {
const files = new Map<string, FileData>([
["script.sh", createMockFileData(["# TODO: shell script task"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(1)
expect(data.todos[0].text).toBe("shell script task")
})
it("should include callId in result", async () => {
const storage = createMockStorage()
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.callId).toMatch(/^get_todos-\d+$/)
})
it("should include execution time in result", async () => {
const storage = createMockStorage()
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.executionTimeMs).toBeGreaterThanOrEqual(0)
})
it("should handle storage errors gracefully", async () => {
const storage = createMockStorage()
;(storage.getAllFiles as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Redis connection failed"),
)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Redis connection failed")
})
it("should find lowercase todo markers", async () => {
const files = new Map<string, FileData>([
["src/index.ts", createMockFileData(["// todo: lowercase"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(1)
expect(data.todos[0].type).toBe("TODO")
})
it("should handle multiple files with todos", async () => {
const files = new Map<string, FileData>([
["src/a.ts", createMockFileData(["// TODO: a1", "// TODO: a2"])],
["src/b.ts", createMockFileData(["// FIXME: b1"])],
["src/c.ts", createMockFileData(["// HACK: c1", "// BUG: c2"])],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.totalTodos).toBe(5)
expect(data.filesWithTodos).toBe(3)
})
it("should correctly identify line numbers", async () => {
const files = new Map<string, FileData>([
[
"src/index.ts",
createMockFileData([
"const a = 1",
"const b = 2",
"// TODO: on line 3",
"const c = 3",
]),
],
])
const storage = createMockStorage(files)
const ctx = createMockContext(storage)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetTodosResult
expect(data.todos[0].line).toBe(3)
})
})
})