Skip to content

Commit 706c4b7

Browse files
committed
fix(e2e): address readonly tool review feedback
1 parent 0f30e86 commit 706c4b7

6 files changed

Lines changed: 48 additions & 40 deletions

File tree

apps/vscode-e2e/src/fixtures/list-files.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ export function addListFilesResultFixtures(mock: InstanceType<typeof LLMock>) {
2424
id: "call_list_files_non_recursive_002",
2525
},
2626
{
27-
userMessagePattern: "LIST_FILES_RECURSIVE_SMOKE",
27+
userMessagePattern: "deep-nested-file.ts is included",
2828
toolName: "list_files",
2929
arguments: '{"path":"list-files-tool-fixture","recursive":true}',
3030
toolCallId: "call_list_files_recursive_001",
31-
expected: ["nested/", "nested/deep/"],
31+
expected: ["nested/", "nested/deep/", "deep-nested-file.ts"],
3232
result: "The recursive listing for `list-files-tool-fixture` reached the nested structure and includes `nested/`, `deep/`, and `deep-nested-file.ts`.",
3333
id: "call_list_files_recursive_002",
3434
},
@@ -42,7 +42,7 @@ export function addListFilesResultFixtures(mock: InstanceType<typeof LLMock>) {
4242
id: "call_list_files_symlink_002",
4343
},
4444
{
45-
userMessagePattern: "LIST_FILES_WORKSPACE_ROOT_SMOKE",
45+
userMessagePattern: "confirm whether list-files-tool-fixture or list-files-symlink-fixture is present",
4646
toolName: "list_files",
4747
arguments: '{"path":".","recursive":false}',
4848
toolCallId: "call_list_files_workspace_root_001",

apps/vscode-e2e/src/fixtures/search-files.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type SearchFilesFixture = {
1515
export function addSearchFilesResultFixtures(mock: InstanceType<typeof LLMock>) {
1616
const fixtures: SearchFilesFixture[] = [
1717
{
18-
userMessagePattern: "SEARCH_FILES_FUNCTIONS_SMOKE",
18+
userMessagePattern: "JavaScript function declarations",
1919
toolName: "search_files",
2020
arguments: '{"path":"search-files-tool-fixture","regex":"function\\\\s+\\\\w+"}',
2121
toolCallId: "call_search_files_functions_001",
@@ -28,7 +28,7 @@ export function addSearchFilesResultFixtures(mock: InstanceType<typeof LLMock>)
2828
id: "call_search_files_functions_002",
2929
},
3030
{
31-
userMessagePattern: "SEARCH_FILES_TODO_SMOKE",
31+
userMessagePattern: "matching TODO entries",
3232
toolName: "search_files",
3333
arguments: '{"path":"search-files-tool-fixture","regex":"TODO.*"}',
3434
toolCallId: "call_search_files_todo_001",
@@ -85,7 +85,7 @@ export function addSearchFilesResultFixtures(mock: InstanceType<typeof LLMock>)
8585
id: "call_search_files_complex_regex_002",
8686
},
8787
{
88-
userMessagePattern: "SEARCH_FILES_NO_MATCH_SMOKE",
88+
userMessagePattern: "nonExistentPattern12345 and report that there are no matches",
8989
toolName: "search_files",
9090
arguments: '{"path":"search-files-tool-fixture","regex":"nonExistentPattern12345"}',
9191
toolCallId: "call_search_files_no_match_001",
@@ -94,7 +94,7 @@ export function addSearchFilesResultFixtures(mock: InstanceType<typeof LLMock>)
9494
id: "call_search_files_no_match_002",
9595
},
9696
{
97-
userMessagePattern: "SEARCH_FILES_CLASS_METHOD_SMOKE",
97+
userMessagePattern: "TypeScript class definitions and async methods",
9898
toolName: "search_files",
9999
arguments:
100100
'{"path":"search-files-tool-fixture","regex":"(class\\\\s+\\\\w+|async\\\\s+\\\\w+)","file_pattern":"*.ts"}',

apps/vscode-e2e/src/suite/tools/list-files.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ This directory contains various files and subdirectories for testing the list_fi
262262
alwaysAllowReadOnly: true,
263263
alwaysAllowReadOnlyOutsideWorkspace: true,
264264
},
265-
text: "LIST_FILES_RECURSIVE_SMOKE",
265+
text: "List every file in the list-files-tool-fixture directory recursively and confirm that the nested path for deep-nested-file.ts is included.",
266266
})
267267

268268
console.log("Task ID:", taskId)
@@ -275,7 +275,8 @@ This directory contains various files and subdirectories for testing the list_fi
275275
m.type === "say" &&
276276
(m.say === "completion_result" || m.say === "text") &&
277277
m.text?.includes("nested/") &&
278-
m.text?.includes("deep/"),
278+
m.text?.includes("deep/") &&
279+
m.text?.includes("deep-nested-file.ts"),
279280
)
280281
assert.ok(completionMessage, "AI should have summarized the recursive directory contents")
281282

@@ -403,7 +404,7 @@ This directory contains various files and subdirectories for testing the list_fi
403404
alwaysAllowReadOnly: true,
404405
alwaysAllowReadOnlyOutsideWorkspace: true,
405406
},
406-
text: "LIST_FILES_WORKSPACE_ROOT_SMOKE",
407+
text: "List the files in the workspace root directory without recursing and confirm whether list-files-tool-fixture or list-files-symlink-fixture is present.",
407408
})
408409

409410
console.log("Task ID:", taskId)
@@ -416,7 +417,7 @@ This directory contains various files and subdirectories for testing the list_fi
416417
(m) =>
417418
m.type === "say" &&
418419
(m.say === "completion_result" || m.say === "text") &&
419-
(m.text?.includes("apps") || m.text?.includes("packages") || m.text?.includes("workspace")),
420+
(m.text?.includes("list-files-tool-fixture") || m.text?.includes("list-files-symlink-fixture")),
420421
)
421422
assert.ok(completionMessage, "AI should have mentioned workspace contents")
422423

apps/vscode-e2e/src/suite/tools/search-files.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ The search should find matches across different file types and provide context f
323323
alwaysAllowReadOnly: true,
324324
alwaysAllowReadOnlyOutsideWorkspace: true,
325325
},
326-
text: "SEARCH_FILES_FUNCTIONS_SMOKE",
326+
text: "Search the search-files-tool-fixture directory for JavaScript function declarations using the regex function\\s+\\w+ and report the function names you find.",
327327
})
328328

329329
console.log("Task ID:", taskId)
@@ -379,7 +379,7 @@ The search should find matches across different file types and provide context f
379379
alwaysAllowReadOnly: true,
380380
alwaysAllowReadOnlyOutsideWorkspace: true,
381381
},
382-
text: "SEARCH_FILES_TODO_SMOKE",
382+
text: "Search the search-files-tool-fixture directory for TODO comments using the regex TODO.* and report the matching TODO entries.",
383383
})
384384

385385
// Wait for task completion
@@ -649,7 +649,7 @@ The search should find matches across different file types and provide context f
649649
alwaysAllowReadOnly: true,
650650
alwaysAllowReadOnlyOutsideWorkspace: true,
651651
},
652-
text: "SEARCH_FILES_NO_MATCH_SMOKE",
652+
text: "Search the search-files-tool-fixture directory for nonExistentPattern12345 and report that there are no matches if the regex finds nothing.",
653653
})
654654

655655
// Wait for task completion
@@ -724,7 +724,7 @@ The search should find matches across different file types and provide context f
724724
alwaysAllowReadOnly: true,
725725
alwaysAllowReadOnlyOutsideWorkspace: true,
726726
},
727-
text: "SEARCH_FILES_CLASS_METHOD_SMOKE",
727+
text: "Search the search-files-tool-fixture directory for TypeScript class definitions and async methods using the regex (class\\s+\\w+|async\\s+\\w+) with file_pattern *.ts, then report what you find.",
728728
})
729729

730730
// Wait for task completion

src/services/glob/__tests__/list-files.spec.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,15 @@ describe("list-files symlink support", () => {
112112
await listFiles(testDir, false, 100)
113113

114114
// Verify that spawn was called with --follow flag (the critical fix)
115-
const [rgPath, args] = mockSpawn.mock.calls[0]
115+
const [rgPath, args, options] = mockSpawn.mock.calls[0]
116116
expect(rgPath).toBe("/mock/path/to/rg")
117117
expect(args).toContain("--files")
118118
expect(args).toContain("--hidden")
119119
expect(args).toContain("--follow") // This is the critical assertion - the fix should add this flag
120120

121-
// Platform-agnostic path check - verify the last argument ends with the expected path
122121
const lastArg = args[args.length - 1]
123-
// On Windows, the path might be resolved to something like D:\test\dir
124-
// On Unix, it would be /test/dir
125-
// So we just check that it ends with the expected segments
126-
expect(lastArg).toMatch(/[/\\]test[/\\]dir$/)
122+
expect(lastArg).toBe(".")
123+
expect(options).toMatchObject({ cwd: testDir })
127124
})
128125

129126
it("should include --follow flag for recursive listings too", async () => {
@@ -137,18 +134,30 @@ describe("list-files symlink support", () => {
137134
await listFiles(testDir, true, 100)
138135

139136
// Verify that spawn was called with --follow flag (the critical fix)
140-
const [rgPath, args] = mockSpawn.mock.calls[0]
137+
const [rgPath, args, options] = mockSpawn.mock.calls[0]
141138
expect(rgPath).toBe("/mock/path/to/rg")
142139
expect(args).toContain("--files")
143140
expect(args).toContain("--hidden")
144141
expect(args).toContain("--follow") // This should be present in recursive mode too
145142

146-
// Platform-agnostic path check - verify the last argument ends with the expected path
147143
const lastArg = args[args.length - 1]
148-
// On Windows, the path might be resolved to something like D:\test\dir
149-
// On Unix, it would be /test/dir
150-
// So we just check that it ends with the expected segments
151-
expect(lastArg).toMatch(/[/\\]test[/\\]dir$/)
144+
expect(lastArg).toBe(".")
145+
expect(options).toMatchObject({ cwd: testDir })
146+
})
147+
148+
it("should keep ignored ancestor directories like /tmp from excluding recursive file results", async () => {
149+
const mockSpawn = vi.mocked(childProcess.spawn)
150+
mockSpawn.mockReturnValue(createMockRipgrepProcess(["nested/deep/deep-nested-file.ts\n"]) as any)
151+
152+
const testDir = "/tmp/roo-test-workspace/list-files-tool-fixture"
153+
154+
const [files] = await listFiles(testDir, true, 100)
155+
156+
const [, args, options] = mockSpawn.mock.calls[0]
157+
expect(args).toContain("!**/tmp/**")
158+
expect(args[args.length - 1]).toBe(".")
159+
expect(options).toMatchObject({ cwd: testDir })
160+
expect(files).toContain(path.join(testDir, "nested", "deep", "deep-nested-file.ts"))
152161
})
153162

154163
it("should ensure first-level directories are included when limit is reached", async () => {

src/services/glob/list-files.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -203,27 +203,28 @@ async function listFilesWithRipgrep(
203203
recursive: boolean,
204204
limit: number,
205205
): Promise<string[]> {
206-
const rgArgs = buildRipgrepArgs(dirPath, recursive)
206+
const absolutePath = path.resolve(dirPath)
207+
const rgArgs = buildRipgrepArgs(".", recursive, dirPath)
207208

208-
const relativePaths = await execRipgrep(rgPath, rgArgs, limit)
209+
const relativePaths = await execRipgrep(rgPath, rgArgs, limit, absolutePath)
209210

210-
// Convert relative paths from ripgrep to absolute paths
211-
// Resolve dirPath once here for the mapping operation
212-
const absolutePath = path.resolve(dirPath)
211+
// Convert relative paths from ripgrep to absolute paths.
212+
// Ripgrep now runs from the target directory so glob exclusions apply within that root
213+
// instead of accidentally matching ignored ancestor path segments like /tmp.
213214
return relativePaths.map((relativePath) => path.resolve(absolutePath, relativePath))
214215
}
215216

216217
/**
217218
* Build appropriate ripgrep arguments based on whether we're doing a recursive search
218219
*/
219-
function buildRipgrepArgs(dirPath: string, recursive: boolean): string[] {
220+
function buildRipgrepArgs(searchPath: string, recursive: boolean, targetDirPath: string): string[] {
220221
// Base arguments to list files
221222
const args = ["--files", "--hidden", "--follow"]
222223

223224
if (recursive) {
224-
return [...args, ...buildRecursiveArgs(dirPath), dirPath]
225+
return [...args, ...buildRecursiveArgs(targetDirPath), searchPath]
225226
} else {
226-
return [...args, ...buildNonRecursiveArgs(), dirPath]
227+
return [...args, ...buildNonRecursiveArgs(), searchPath]
227228
}
228229
}
229230

@@ -646,12 +647,9 @@ function formatAndCombineResults(files: string[], directories: string[], limit:
646647
/**
647648
* Execute ripgrep command and return list of files
648649
*/
649-
async function execRipgrep(rgPath: string, args: string[], limit: number): Promise<string[]> {
650+
async function execRipgrep(rgPath: string, args: string[], limit: number, cwd?: string): Promise<string[]> {
650651
return new Promise((resolve, reject) => {
651-
// Extract the directory path from args (it's the last argument)
652-
const searchDir = args[args.length - 1]
653-
654-
const rgProcess = childProcess.spawn(rgPath, args)
652+
const rgProcess = childProcess.spawn(rgPath, args, cwd ? { cwd } : undefined)
655653
let output = ""
656654
let results: string[] = []
657655

0 commit comments

Comments
 (0)