Skip to content

Commit f4721b2

Browse files
committed
ListCodeDefinitionNamesHandler.test
1 parent 7f4dd09 commit f4721b2

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import * as path from "path"
2+
import * as fs from "fs/promises"
3+
import { ListCodeDefinitionNamesHandler } from "../ListCodeDefinitionNamesHandler"
4+
import { Cline } from "../../../Cline"
5+
import { ToolUse } from "../../../assistant-message"
6+
import { formatResponse } from "../../../prompts/responses"
7+
import { RooIgnoreController } from "../../../ignore/RooIgnoreController"
8+
import {
9+
parseSourceCodeDefinitionsForFile,
10+
parseSourceCodeForDefinitionsTopLevel,
11+
} from "../../../../services/tree-sitter"
12+
import { telemetryService } from "../../../../services/telemetry/TelemetryService"
13+
import { getReadablePath } from "../../../../utils/path"
14+
15+
// --- Mocks ---
16+
jest.mock("../../../Cline")
17+
const MockCline = Cline as jest.MockedClass<typeof Cline>
18+
19+
jest.mock("fs/promises", () => ({
20+
stat: jest.fn(), // Will configure per test
21+
}))
22+
const mockFsStat = fs.stat as jest.Mock
23+
24+
jest.mock("../../../ignore/RooIgnoreController")
25+
const MockRooIgnoreController = RooIgnoreController as jest.MockedClass<typeof RooIgnoreController>
26+
27+
jest.mock("../../../../services/tree-sitter")
28+
const mockParseFile = parseSourceCodeDefinitionsForFile as jest.Mock
29+
const mockParseDir = parseSourceCodeForDefinitionsTopLevel as jest.Mock
30+
31+
jest.mock("../../../prompts/responses", () => ({
32+
formatResponse: {
33+
toolError: jest.fn((msg) => `ERROR: ${msg}`),
34+
toolResult: jest.fn((text) => text), // Simple mock
35+
rooIgnoreError: jest.fn((file) => `RooIgnore Error: ${file}`),
36+
},
37+
}))
38+
39+
jest.mock("../../../../services/telemetry/TelemetryService", () => ({
40+
telemetryService: {
41+
captureToolUsage: jest.fn(),
42+
},
43+
}))
44+
45+
jest.mock("../../../../utils/path", () => ({
46+
getReadablePath: jest.fn((cwd, p) => p || "mock/path"), // Simple mock
47+
}))
48+
49+
describe("ListCodeDefinitionNamesHandler", () => {
50+
let mockClineInstance: jest.MockedObject<Cline>
51+
let mockRooIgnoreControllerInstance: jest.MockedObject<RooIgnoreController>
52+
let mockToolUse: ToolUse
53+
54+
beforeEach(() => {
55+
jest.clearAllMocks()
56+
57+
mockRooIgnoreControllerInstance = new MockRooIgnoreController(
58+
"/workspace",
59+
) as jest.MockedObject<RooIgnoreController>
60+
// Explicitly assign a mock function to validateAccess on the instance
61+
mockRooIgnoreControllerInstance.validateAccess = jest.fn().mockReturnValue(true) // Default: access allowed
62+
63+
mockClineInstance = {
64+
cwd: "/workspace",
65+
consecutiveMistakeCount: 0,
66+
taskId: "test-task-id",
67+
rooIgnoreController: mockRooIgnoreControllerInstance,
68+
ask: jest.fn(() => Promise.resolve({})),
69+
say: jest.fn(() => Promise.resolve()),
70+
pushToolResult: jest.fn(() => Promise.resolve()),
71+
handleErrorHelper: jest.fn(() => Promise.resolve()),
72+
sayAndCreateMissingParamError: jest.fn((tool, param) => Promise.resolve(`Missing ${param}`)),
73+
askApprovalHelper: jest.fn(() => Promise.resolve(true)), // Default approval
74+
providerRef: { deref: () => ({ getState: () => Promise.resolve({}) }) },
75+
emit: jest.fn(),
76+
getTokenUsage: jest.fn(() => ({})),
77+
removeClosingTag: jest.fn((tag, value) => value),
78+
} as unknown as jest.MockedObject<Cline>
79+
80+
mockToolUse = {
81+
type: "tool_use",
82+
name: "list_code_definition_names",
83+
params: {
84+
path: "src/some_file.ts",
85+
},
86+
partial: false,
87+
}
88+
89+
// Default stat mock (file)
90+
mockFsStat.mockResolvedValue({
91+
isFile: () => true,
92+
isDirectory: () => false,
93+
})
94+
mockParseFile.mockResolvedValue("Parsed file definitions")
95+
mockParseDir.mockResolvedValue("Parsed directory definitions")
96+
})
97+
98+
// --- Test validateParams ---
99+
test("validateParams should throw if path is missing", () => {
100+
delete mockToolUse.params.path
101+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
102+
expect(() => handler.validateParams()).toThrow("Missing required parameter 'path'")
103+
})
104+
105+
test("validateParams should not throw if path is present", () => {
106+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
107+
expect(() => handler.validateParams()).not.toThrow()
108+
})
109+
110+
// --- Test handlePartial ---
111+
test("handlePartial should call ask with tool info", async () => {
112+
mockToolUse.partial = true
113+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
114+
await handler.handle()
115+
expect(mockClineInstance.ask).toHaveBeenCalledWith(
116+
"tool",
117+
JSON.stringify({
118+
tool: "listCodeDefinitionNames",
119+
path: mockToolUse.params.path,
120+
content: "",
121+
}),
122+
true,
123+
)
124+
})
125+
126+
// --- Test handleComplete ---
127+
test("handleComplete should fail if path param is missing", async () => {
128+
delete mockToolUse.params.path
129+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
130+
await handler.handle()
131+
expect(mockClineInstance.sayAndCreateMissingParamError).toHaveBeenCalledWith(
132+
"list_code_definition_names",
133+
"path",
134+
)
135+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, "Missing path")
136+
expect(mockClineInstance.consecutiveMistakeCount).toBe(1)
137+
})
138+
139+
test("handleComplete should parse file, ask approval, and push result", async () => {
140+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
141+
await handler.handle()
142+
143+
expect(mockFsStat).toHaveBeenCalledWith(path.resolve("/workspace", "src/some_file.ts"))
144+
expect(mockRooIgnoreControllerInstance.validateAccess).toHaveBeenCalledWith("src/some_file.ts")
145+
expect(mockParseFile).toHaveBeenCalledWith(
146+
path.resolve("/workspace", "src/some_file.ts"),
147+
mockRooIgnoreControllerInstance,
148+
)
149+
expect(mockParseDir).not.toHaveBeenCalled()
150+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
151+
mockToolUse,
152+
"tool",
153+
expect.stringContaining('"content":"Parsed file definitions"'),
154+
)
155+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, "Parsed file definitions")
156+
expect(telemetryService.captureToolUsage).toHaveBeenCalledWith(
157+
mockClineInstance.taskId,
158+
"list_code_definition_names",
159+
)
160+
expect(mockClineInstance.consecutiveMistakeCount).toBe(0)
161+
})
162+
163+
test("handleComplete should parse directory, ask approval, and push result", async () => {
164+
mockToolUse.params.path = "src/some_dir"
165+
mockFsStat.mockResolvedValue({ isFile: () => false, isDirectory: () => true }) // Mock as directory
166+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
167+
await handler.handle()
168+
169+
expect(mockFsStat).toHaveBeenCalledWith(path.resolve("/workspace", "src/some_dir"))
170+
expect(mockRooIgnoreControllerInstance.validateAccess).not.toHaveBeenCalled() // Not called for dir
171+
expect(mockParseDir).toHaveBeenCalledWith(
172+
path.resolve("/workspace", "src/some_dir"),
173+
mockRooIgnoreControllerInstance,
174+
)
175+
expect(mockParseFile).not.toHaveBeenCalled()
176+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
177+
mockToolUse,
178+
"tool",
179+
expect.stringContaining('"content":"Parsed directory definitions"'),
180+
)
181+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, "Parsed directory definitions")
182+
expect(telemetryService.captureToolUsage).toHaveBeenCalledWith(
183+
mockClineInstance.taskId,
184+
"list_code_definition_names",
185+
)
186+
})
187+
188+
test("handleComplete should handle path not existing", async () => {
189+
const error = new Error("Not found") as NodeJS.ErrnoException
190+
error.code = "ENOENT"
191+
mockFsStat.mockRejectedValue(error)
192+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
193+
await handler.handle()
194+
195+
expect(mockFsStat).toHaveBeenCalled()
196+
expect(mockParseFile).not.toHaveBeenCalled()
197+
expect(mockParseDir).not.toHaveBeenCalled()
198+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
199+
mockToolUse,
200+
"tool",
201+
expect.stringContaining("does not exist or cannot be accessed"),
202+
)
203+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(
204+
mockToolUse,
205+
expect.stringContaining("does not exist or cannot be accessed"),
206+
)
207+
})
208+
209+
test("handleComplete should handle path being neither file nor directory", async () => {
210+
mockFsStat.mockResolvedValue({ isFile: () => false, isDirectory: () => false }) // Mock as neither
211+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
212+
await handler.handle()
213+
214+
expect(mockFsStat).toHaveBeenCalled()
215+
expect(mockParseFile).not.toHaveBeenCalled()
216+
expect(mockParseDir).not.toHaveBeenCalled()
217+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
218+
mockToolUse,
219+
"tool",
220+
expect.stringContaining("neither a file nor a directory"),
221+
)
222+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(
223+
mockToolUse,
224+
expect.stringContaining("neither a file nor a directory"),
225+
)
226+
})
227+
228+
test("handleComplete should fail if file access denied by rooignore", async () => {
229+
mockRooIgnoreControllerInstance.validateAccess.mockReturnValue(false) // Deny access
230+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
231+
await handler.handle()
232+
233+
expect(mockFsStat).toHaveBeenCalled()
234+
expect(mockRooIgnoreControllerInstance.validateAccess).toHaveBeenCalledWith("src/some_file.ts")
235+
expect(mockParseFile).not.toHaveBeenCalled()
236+
expect(mockParseDir).not.toHaveBeenCalled()
237+
expect(mockClineInstance.say).toHaveBeenCalledWith("rooignore_error", "src/some_file.ts")
238+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(
239+
mockToolUse,
240+
"ERROR: RooIgnore Error: src/some_file.ts",
241+
)
242+
expect(mockClineInstance.askApprovalHelper).not.toHaveBeenCalled()
243+
})
244+
245+
test("handleComplete should skip push if approval denied", async () => {
246+
;(mockClineInstance.askApprovalHelper as jest.Mock).mockResolvedValue(false) // Deny approval
247+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
248+
await handler.handle()
249+
250+
expect(mockFsStat).toHaveBeenCalled()
251+
expect(mockParseFile).toHaveBeenCalled() // Parsing still happens before approval
252+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalled()
253+
expect(mockClineInstance.pushToolResult).not.toHaveBeenCalled() // Handled by helper
254+
expect(telemetryService.captureToolUsage).not.toHaveBeenCalled()
255+
})
256+
257+
test("handleComplete should handle errors during parsing", async () => {
258+
const parseError = new Error("Tree-sitter failed")
259+
mockParseFile.mockRejectedValue(parseError) // Make parsing throw
260+
const handler = new ListCodeDefinitionNamesHandler(mockClineInstance, mockToolUse)
261+
await handler.handle()
262+
263+
expect(mockFsStat).toHaveBeenCalled()
264+
expect(mockParseFile).toHaveBeenCalled()
265+
expect(mockClineInstance.askApprovalHelper).not.toHaveBeenCalled() // Error before approval
266+
expect(mockClineInstance.handleErrorHelper).toHaveBeenCalledWith(
267+
mockToolUse,
268+
"parsing source code definitions",
269+
parseError,
270+
)
271+
expect(mockClineInstance.pushToolResult).not.toHaveBeenCalled() // Handled by error helper
272+
})
273+
})

0 commit comments

Comments
 (0)