Skip to content

Commit 0ef1abd

Browse files
committed
ListFilesHandler.test
1 parent f4721b2 commit 0ef1abd

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import * as path from "path"
2+
import { ListFilesHandler } from "../ListFilesHandler"
3+
import { Cline } from "../../../Cline"
4+
import { ToolUse } from "../../../assistant-message"
5+
import { formatResponse } from "../../../prompts/responses"
6+
import { listFiles } from "../../../../services/glob/list-files" // Import the function to mock
7+
import { RooIgnoreController } from "../../../ignore/RooIgnoreController"
8+
import { telemetryService } from "../../../../services/telemetry/TelemetryService"
9+
import { getReadablePath } from "../../../../utils/path"
10+
11+
// --- Mocks ---
12+
jest.mock("../../../Cline")
13+
const MockCline = Cline as jest.MockedClass<typeof Cline>
14+
15+
jest.mock("../../../../services/glob/list-files")
16+
const mockListFiles = listFiles as jest.Mock
17+
18+
jest.mock("../../../ignore/RooIgnoreController")
19+
const MockRooIgnoreController = RooIgnoreController as jest.MockedClass<typeof RooIgnoreController>
20+
21+
jest.mock("../../../prompts/responses", () => ({
22+
formatResponse: {
23+
toolError: jest.fn((msg) => `ERROR: ${msg}`),
24+
toolResult: jest.fn((text) => text), // Simple mock
25+
formatFilesList: jest.fn(
26+
(absPath, files, limitHit, ignoreController, showIgnored) =>
27+
`Formatted list for ${absPath}: ${files.join(", ")}${limitHit ? " (limit hit)" : ""}${showIgnored ? " (showing ignored)" : ""}`,
28+
),
29+
},
30+
}))
31+
32+
jest.mock("../../../../services/telemetry/TelemetryService", () => ({
33+
telemetryService: {
34+
captureToolUsage: jest.fn(),
35+
},
36+
}))
37+
38+
jest.mock("../../../../utils/path", () => ({
39+
getReadablePath: jest.fn((cwd, p) => p || "mock/path"), // Simple mock
40+
}))
41+
42+
describe("ListFilesHandler", () => {
43+
let mockClineInstance: jest.MockedObject<Cline>
44+
let mockRooIgnoreControllerInstance: jest.MockedObject<RooIgnoreController>
45+
let mockToolUse: ToolUse
46+
47+
beforeEach(() => {
48+
jest.clearAllMocks()
49+
50+
mockRooIgnoreControllerInstance = new MockRooIgnoreController(
51+
"/workspace",
52+
) as jest.MockedObject<RooIgnoreController>
53+
// No methods needed for default mock in this handler
54+
55+
mockClineInstance = {
56+
cwd: "/workspace",
57+
consecutiveMistakeCount: 0,
58+
taskId: "test-task-id",
59+
rooIgnoreController: mockRooIgnoreControllerInstance,
60+
ask: jest.fn(() => Promise.resolve({})),
61+
say: jest.fn(() => Promise.resolve()),
62+
pushToolResult: jest.fn(() => Promise.resolve()),
63+
handleErrorHelper: jest.fn(() => Promise.resolve()),
64+
sayAndCreateMissingParamError: jest.fn((tool, param) => Promise.resolve(`Missing ${param}`)),
65+
askApprovalHelper: jest.fn(() => Promise.resolve(true)), // Default approval
66+
providerRef: { deref: () => ({ getState: () => Promise.resolve({ showRooIgnoredFiles: true }) }) }, // Mock provider state
67+
emit: jest.fn(),
68+
getTokenUsage: jest.fn(() => ({})),
69+
removeClosingTag: jest.fn((tag, value) => value),
70+
} as unknown as jest.MockedObject<Cline>
71+
72+
mockToolUse = {
73+
type: "tool_use",
74+
name: "list_files",
75+
params: {
76+
path: "src/some_dir",
77+
recursive: "false", // Default non-recursive
78+
},
79+
partial: false,
80+
}
81+
82+
// Default listFiles mock
83+
mockListFiles.mockResolvedValue([["file1.ts", "file2.js"], false]) // [files, didHitLimit]
84+
})
85+
86+
// --- Test validateParams ---
87+
test("validateParams should throw if path is missing", () => {
88+
delete mockToolUse.params.path
89+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
90+
expect(() => handler.validateParams()).toThrow("Missing required parameter 'path'")
91+
})
92+
93+
test("validateParams should not throw if path is present", () => {
94+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
95+
expect(() => handler.validateParams()).not.toThrow()
96+
})
97+
98+
// --- Test handlePartial ---
99+
test("handlePartial should call ask with listFilesTopLevel for non-recursive", async () => {
100+
mockToolUse.partial = true
101+
mockToolUse.params.recursive = "false"
102+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
103+
await handler.handle()
104+
expect(mockClineInstance.ask).toHaveBeenCalledWith(
105+
"tool",
106+
JSON.stringify({
107+
tool: "listFilesTopLevel",
108+
path: mockToolUse.params.path,
109+
content: "",
110+
}),
111+
true,
112+
)
113+
})
114+
115+
test("handlePartial should call ask with listFilesRecursive for recursive", async () => {
116+
mockToolUse.partial = true
117+
mockToolUse.params.recursive = "true"
118+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
119+
await handler.handle()
120+
expect(mockClineInstance.ask).toHaveBeenCalledWith(
121+
"tool",
122+
JSON.stringify({
123+
tool: "listFilesRecursive",
124+
path: mockToolUse.params.path,
125+
content: "",
126+
}),
127+
true,
128+
)
129+
})
130+
131+
// --- Test handleComplete ---
132+
test("handleComplete should fail if path param is missing", async () => {
133+
delete mockToolUse.params.path
134+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
135+
await handler.handle()
136+
expect(mockClineInstance.sayAndCreateMissingParamError).toHaveBeenCalledWith("list_files", "path")
137+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, "Missing path")
138+
expect(mockClineInstance.consecutiveMistakeCount).toBe(1)
139+
})
140+
141+
test("handleComplete should call listFiles (non-recursive), format, ask approval, and push result", async () => {
142+
mockToolUse.params.recursive = "false"
143+
const expectedFiles = ["fileA.txt", "fileB.log"]
144+
const expectedLimitHit = false
145+
mockListFiles.mockResolvedValue([expectedFiles, expectedLimitHit])
146+
const expectedFormattedResult = `Formatted list for /workspace/src/some_dir: ${expectedFiles.join(", ")} (showing ignored)`
147+
;(formatResponse.formatFilesList as jest.Mock).mockReturnValue(expectedFormattedResult)
148+
149+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
150+
await handler.handle()
151+
152+
expect(mockListFiles).toHaveBeenCalledWith(path.resolve("/workspace", "src/some_dir"), false, 200)
153+
expect(formatResponse.formatFilesList).toHaveBeenCalledWith(
154+
path.resolve("/workspace", "src/some_dir"),
155+
expectedFiles,
156+
expectedLimitHit,
157+
mockRooIgnoreControllerInstance,
158+
true, // showRooIgnoredFiles from mock state
159+
)
160+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
161+
mockToolUse,
162+
"tool",
163+
expect.stringContaining(`"content":"${expectedFormattedResult}"`),
164+
)
165+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, expectedFormattedResult)
166+
expect(telemetryService.captureToolUsage).toHaveBeenCalledWith(mockClineInstance.taskId, "list_files")
167+
expect(mockClineInstance.consecutiveMistakeCount).toBe(0)
168+
})
169+
170+
test("handleComplete should call listFiles (recursive), format, ask approval, and push result", async () => {
171+
mockToolUse.params.recursive = "true"
172+
const expectedFiles = ["fileA.txt", "subdir/fileC.ts"]
173+
const expectedLimitHit = true
174+
mockListFiles.mockResolvedValue([expectedFiles, expectedLimitHit])
175+
const expectedFormattedResult = `Formatted list for /workspace/src/some_dir: ${expectedFiles.join(", ")} (limit hit) (showing ignored)`
176+
;(formatResponse.formatFilesList as jest.Mock).mockReturnValue(expectedFormattedResult)
177+
178+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
179+
await handler.handle()
180+
181+
expect(mockListFiles).toHaveBeenCalledWith(path.resolve("/workspace", "src/some_dir"), true, 200) // Recursive true
182+
expect(formatResponse.formatFilesList).toHaveBeenCalledWith(
183+
path.resolve("/workspace", "src/some_dir"),
184+
expectedFiles,
185+
expectedLimitHit,
186+
mockRooIgnoreControllerInstance,
187+
true,
188+
)
189+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
190+
mockToolUse,
191+
"tool",
192+
expect.stringContaining(`"content":"${expectedFormattedResult}"`),
193+
)
194+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, expectedFormattedResult)
195+
expect(telemetryService.captureToolUsage).toHaveBeenCalledWith(mockClineInstance.taskId, "list_files")
196+
})
197+
198+
test("handleComplete should skip push if approval denied", async () => {
199+
;(mockClineInstance.askApprovalHelper as jest.Mock).mockResolvedValue(false) // Deny approval
200+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
201+
await handler.handle()
202+
203+
expect(mockListFiles).toHaveBeenCalled() // Listing still happens
204+
expect(formatResponse.formatFilesList).toHaveBeenCalled()
205+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalled()
206+
expect(mockClineInstance.pushToolResult).not.toHaveBeenCalled() // Handled by helper
207+
expect(telemetryService.captureToolUsage).not.toHaveBeenCalled()
208+
})
209+
210+
test("handleComplete should handle errors during listFiles", async () => {
211+
const listError = new Error("Failed to list")
212+
mockListFiles.mockRejectedValue(listError) // Make listing throw
213+
const handler = new ListFilesHandler(mockClineInstance, mockToolUse)
214+
await handler.handle()
215+
216+
expect(mockListFiles).toHaveBeenCalled()
217+
expect(formatResponse.formatFilesList).not.toHaveBeenCalled() // Error before formatting
218+
expect(mockClineInstance.askApprovalHelper).not.toHaveBeenCalled() // Error before approval
219+
expect(mockClineInstance.handleErrorHelper).toHaveBeenCalledWith(mockToolUse, "listing files", listError)
220+
expect(mockClineInstance.pushToolResult).not.toHaveBeenCalled() // Handled by error helper
221+
})
222+
})

0 commit comments

Comments
 (0)