diff --git a/apps/vscode-e2e/fixtures/task-hello-world.json b/apps/vscode-e2e/fixtures/task-hello-world.json index 19b3da57fc..b150f5f0aa 100644 --- a/apps/vscode-e2e/fixtures/task-hello-world.json +++ b/apps/vscode-e2e/fixtures/task-hello-world.json @@ -8,7 +8,7 @@ "toolCalls": [ { "name": "attempt_completion", - "arguments": "{\"result\":\"My name is Roo! I'm your AI coding assistant, here to help you with development tasks.\"}", + "arguments": "{\"result\":\"My name is Zoo! I'm your AI coding assistant, here to help you with development tasks.\"}", "id": "call_task_hello_world_001" } ] diff --git a/apps/vscode-e2e/src/fixtures/fixture-utils.ts b/apps/vscode-e2e/src/fixtures/fixture-utils.ts new file mode 100644 index 0000000000..13a7d2a609 --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/fixture-utils.ts @@ -0,0 +1,15 @@ +import type { ChatCompletionRequest, ChatMessage } from "@copilotkit/aimock" + +export function toolResultContains(req: ChatCompletionRequest, toolCallId: string, expected: string[]) { + const messages = Array.isArray(req?.messages) ? req.messages : [] + const toolMessage = messages.find( + (message: ChatMessage) => message?.role === "tool" && message.tool_call_id === toolCallId, + ) + + const content = toolMessage?.content + if (typeof content !== "string") { + return false + } + + return expected.every((text) => content.includes(text)) +} diff --git a/apps/vscode-e2e/src/fixtures/list-files.ts b/apps/vscode-e2e/src/fixtures/list-files.ts new file mode 100644 index 0000000000..9c89147276 --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/list-files.ts @@ -0,0 +1,87 @@ +import { LLMock } from "@copilotkit/aimock" + +import { toolResultContains } from "./fixture-utils" + +type ListFilesFixture = { + userMessagePattern: string + toolName: string + arguments: string + toolCallId: string + expected: string[] + result: string + id: string +} + +export function addListFilesResultFixtures(mock: InstanceType) { + const fixtures: ListFilesFixture[] = [ + { + userMessagePattern: "without recursing into subdirectories", + toolName: "list_files", + arguments: '{"path":"list-files-tool-fixture","recursive":false}', + toolCallId: "call_list_files_non_recursive_001", + expected: ["root-file-1.txt", ".hidden-file", "nested/"], + result: "The non-recursive listing for `list-files-tool-fixture` includes `root-file-1.txt`, `root-file-2.js`, `config.yaml`, `README.md`, `.hidden-file`, and the `nested/` directory.", + id: "call_list_files_non_recursive_002", + }, + { + userMessagePattern: "deep-nested-file.ts is included", + toolName: "list_files", + arguments: '{"path":"list-files-tool-fixture","recursive":true}', + toolCallId: "call_list_files_recursive_001", + expected: ["nested/", "nested/deep/", "deep-nested-file.ts"], + result: "The recursive listing for `list-files-tool-fixture` reached the nested structure and includes `nested/`, `nested/deep/`, and `deep-nested-file.ts`.", + id: "call_list_files_recursive_002", + }, + { + userMessagePattern: "path='list-files-symlink-fixture'", + toolName: "list_files", + arguments: '{"path":"list-files-symlink-fixture","recursive":false}', + toolCallId: "call_list_files_symlink_001", + expected: ["link-to-file.txt", "source/"], + result: "The symlink fixture listing shows the original `source/` directory and its `source-file.txt`, plus the symlink entry `link-to-file.txt` in `list-files-symlink-fixture`.", + id: "call_list_files_symlink_002", + }, + { + userMessagePattern: "confirm whether list-files-tool-fixture or list-files-symlink-fixture is present", + toolName: "list_files", + arguments: '{"path":".","recursive":false}', + toolCallId: "call_list_files_workspace_root_001", + expected: ["list-files-tool-fixture/"], + result: "The workspace root currently contains the `list-files-tool-fixture/` and `list-files-symlink-fixture/` test directories.", + id: "call_list_files_workspace_root_002", + }, + ] + + for (const fixture of fixtures) { + mock.addFixture({ + match: { + userMessage: new RegExp(fixture.userMessagePattern), + }, + response: { + toolCalls: [ + { + name: fixture.toolName, + arguments: fixture.arguments, + id: fixture.toolCallId, + }, + ], + }, + }) + + mock.addFixture({ + match: { + toolCallId: fixture.toolCallId, + predicate: (req) => toolResultContains(req, fixture.toolCallId, fixture.expected), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: fixture.result }), + id: fixture.id, + }, + ], + }, + }) + } +} diff --git a/apps/vscode-e2e/src/fixtures/search-files.ts b/apps/vscode-e2e/src/fixtures/search-files.ts new file mode 100644 index 0000000000..6df43aec58 --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/search-files.ts @@ -0,0 +1,144 @@ +import { LLMock } from "@copilotkit/aimock" + +import { toolResultContains } from "./fixture-utils" + +type SearchFilesFixture = { + userMessagePattern: string + toolName: string + arguments: string + toolCallId: string + expected: string[] + result: string + id: string +} + +export function addSearchFilesResultFixtures(mock: InstanceType) { + const fixtures: SearchFilesFixture[] = [ + { + userMessagePattern: "JavaScript function declarations", + toolName: "search_files", + arguments: '{"path":"search-files-tool-fixture","regex":"function\\\\s+\\\\w+"}', + toolCallId: "call_search_files_functions_001", + expected: [ + "# search-files-tool-fixture/search-fixture.js", + "function calculateTotal(items) {", + "function validateUser(user) {", + ], + result: "The function search found declarations including `calculateTotal`, `validateUser`, and `formatCurrency`.", + id: "call_search_files_functions_002", + }, + { + userMessagePattern: "TODO comments using the regex TODO", + toolName: "search_files", + arguments: '{"path":"search-files-tool-fixture","regex":"TODO.*"}', + toolCallId: "call_search_files_todo_001", + expected: [ + "# search-files-tool-fixture/search-fixture.js", + "// TODO: Add more validation functions", + "// TODO: Implement user fetching", + ], + result: "The TODO search found matching TODO entries in the fixture files, including the validation and user-fetching notes.", + id: "call_search_files_todo_002", + }, + { + userMessagePattern: "TypeScript interfaces you find", + toolName: "search_files", + arguments: '{"path":"search-files-tool-fixture","regex":"interface\\\\s+\\\\w+","file_pattern":"*.ts"}', + toolCallId: "call_search_files_typescript_001", + expected: ["# search-files-tool-fixture/search-fixture.ts", "interface User {", "interface Product {"], + result: "The TypeScript-only search found the `User` and `Product` interface definitions.", + id: "call_search_files_typescript_002", + }, + { + userMessagePattern: "JSON configuration keys", + toolName: "search_files", + arguments: '{"path":"search-files-tool-fixture","regex":"\\"\\\\w+\\":\\\\s*","file_pattern":"*.json"}', + toolCallId: "call_search_files_json_001", + expected: ["# search-files-tool-fixture/search-config.json", '"name": "test-app",', '"dependencies": {'], + result: "The JSON search found configuration keys such as `name`, `version`, and `dependencies` in `search-config.json`.", + id: "call_search_files_json_002", + }, + { + userMessagePattern: "formatCurrency and debounce", + toolName: "search_files", + arguments: '{"path":"search-files-tool-fixture","regex":"function\\\\s+(format|debounce)"}', + toolCallId: "call_search_files_nested_001", + expected: [ + "# search-files-tool-fixture/nested/nested-search.js", + "function formatCurrency(amount) {", + "function debounce(func, wait) {", + ], + result: "The nested-directory search found the utility functions `formatCurrency` and `debounce`.", + id: "call_search_files_nested_002", + }, + { + userMessagePattern: "import and export statements", + toolName: "search_files", + arguments: '{"path":"search-files-tool-fixture","regex":"(import|export).*","file_pattern":"*.{js,ts}"}', + toolCallId: "call_search_files_complex_regex_001", + expected: [ + "# search-files-tool-fixture/search-fixture.js", + "export { calculateTotal, validateUser }", + "module.exports = { formatCurrency, debounce }", + ], + result: "The import/export search found the `export` statement in the JavaScript fixture module.", + id: "call_search_files_complex_regex_002", + }, + { + userMessagePattern: "nonExistentPattern12345 and report that there are no matches", + toolName: "search_files", + arguments: '{"path":"search-files-tool-fixture","regex":"nonExistentPattern12345"}', + toolCallId: "call_search_files_no_match_001", + expected: ["No results found"], + result: "No matches were found for `nonExistentPattern12345` in the search fixture directory.", + id: "call_search_files_no_match_002", + }, + { + userMessagePattern: "TypeScript class definitions and async methods", + toolName: "search_files", + arguments: + '{"path":"search-files-tool-fixture","regex":"(class\\\\s+\\\\w+|async\\\\s+\\\\w+)","file_pattern":"*.ts"}', + toolCallId: "call_search_files_class_method_001", + expected: [ + "# search-files-tool-fixture/search-fixture.ts", + "class UserService {", + "async getUser(id: number): Promise {", + ], + result: "The class-and-method search found `UserService` and its async `getUser` method in the TypeScript fixture.", + id: "call_search_files_class_method_002", + }, + ] + + for (const fixture of fixtures) { + mock.addFixture({ + match: { + userMessage: new RegExp(fixture.userMessagePattern), + }, + response: { + toolCalls: [ + { + name: fixture.toolName, + arguments: fixture.arguments, + id: fixture.toolCallId, + }, + ], + }, + }) + + mock.addFixture({ + match: { + toolCallId: fixture.toolCallId, + predicate: (req) => toolResultContains(req, fixture.toolCallId, fixture.expected), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: fixture.result }), + id: fixture.id, + }, + ], + }, + }) + } +} diff --git a/apps/vscode-e2e/src/runTest.ts b/apps/vscode-e2e/src/runTest.ts index 27e14a0ea8..1e53b885cb 100644 --- a/apps/vscode-e2e/src/runTest.ts +++ b/apps/vscode-e2e/src/runTest.ts @@ -5,7 +5,9 @@ import * as fs from "fs/promises" import { runTests } from "@vscode/test-electron" import { LLMock } from "@copilotkit/aimock" +import { addListFilesResultFixtures } from "./fixtures/list-files" import { addReadFileResultFixtures } from "./fixtures/read-file" +import { addSearchFilesResultFixtures } from "./fixtures/search-files" function getCliFlagValue(flag: string) { return process.argv.find((arg, index) => process.argv[index - 1] === flag) @@ -77,7 +79,9 @@ async function main() { mock.loadFixtureDir(fixturesDir) if (!isRecord) { + addListFilesResultFixtures(mock) addReadFileResultFixtures(mock) + addSearchFilesResultFixtures(mock) // The modes test (switch_mode → ask) triggers a second API call whose last // user message starts with directly — no diff --git a/apps/vscode-e2e/src/suite/index.ts b/apps/vscode-e2e/src/suite/index.ts index a0a313f71d..4c63b70787 100644 --- a/apps/vscode-e2e/src/suite/index.ts +++ b/apps/vscode-e2e/src/suite/index.ts @@ -3,7 +3,7 @@ import Mocha from "mocha" import { glob } from "glob" import * as vscode from "vscode" -import type { RooCodeAPI, RooCodeEventName } from "@roo-code/types" +import { RooCodeEventName, type RooCodeAPI } from "@roo-code/types" import { waitFor } from "./utils" @@ -33,12 +33,20 @@ export async function run() { // Automatically approve completion_result asks so tests don't stall waiting // for a button that the webview routes to "start new task" rather than "yes". - api.on("message" as RooCodeEventName.Message, ({ message }) => { + api.on(RooCodeEventName.Message, ({ message }) => { if (message.type === "ask" && message.ask === "completion_result") { api.approveCurrentAsk() } }) + if (!aimockUrl) { + api.on(RooCodeEventName.Message, ({ message }) => { + if (message.type === "say" && !message.partial) { + console.log(`[say:${message.say}]`, message.text?.slice(0, 300)) + } + }) + } + globalThis.api = api const mochaOptions: Mocha.MochaOptions = { diff --git a/apps/vscode-e2e/src/suite/task.test.ts b/apps/vscode-e2e/src/suite/task.test.ts index 10e4e4f9a6..8d10053f6d 100644 --- a/apps/vscode-e2e/src/suite/task.test.ts +++ b/apps/vscode-e2e/src/suite/task.test.ts @@ -28,9 +28,9 @@ suite("Roo Code Task", function () { assert.ok( !!messages.find( - ({ say, text }) => (say === "completion_result" || say === "text") && text?.includes("My name is Roo"), + ({ say, text }) => (say === "completion_result" || say === "text") && text?.includes("My name is Zoo"), ), - `Completion should include "My name is Roo"`, + `Completion should include "My name is Zoo"`, ) }) }) diff --git a/apps/vscode-e2e/src/suite/tools/list-files.test.ts b/apps/vscode-e2e/src/suite/tools/list-files.test.ts index 386433e7b8..7cf5a8abca 100644 --- a/apps/vscode-e2e/src/suite/tools/list-files.test.ts +++ b/apps/vscode-e2e/src/suite/tools/list-files.test.ts @@ -8,7 +8,10 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" -suite.skip("Roo Code list_files Tool", function () { +const TEST_DIR_NAME = "list-files-tool-fixture" +const SYMLINK_TEST_DIR_NAME = "list-files-symlink-fixture" + +suite("Roo Code list_files Tool", function () { setDefaultSuiteTimeout(this) let workspaceDir: string @@ -36,11 +39,13 @@ suite.skip("Roo Code list_files Tool", function () { console.log("Workspace directory:", workspaceDir) // Create test directory structure - const testDirName = `list-files-test-${Date.now()}` - const testDir = path.join(workspaceDir, testDirName) + const testDir = path.join(workspaceDir, TEST_DIR_NAME) const nestedDir = path.join(testDir, "nested") const deepNestedDir = path.join(nestedDir, "deep") + await fs.rm(testDir, { recursive: true, force: true }) + await fs.rm(path.join(workspaceDir, SYMLINK_TEST_DIR_NAME), { recursive: true, force: true }) + testFiles = { rootFile1: path.join(testDir, "root-file-1.txt"), rootFile2: path.join(testDir, "root-file-2.js"), @@ -177,35 +182,10 @@ This directory contains various files and subdirectories for testing the list_fi const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false - let listResults: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution and capture results - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("list_files")) { - toolExecuted = true - console.log("list_files tool executed:", text.substring(0, 200)) - - // Extract list results from the tool execution - try { - const jsonMatch = text.match(/\{"request":".*?"\}/) - if (jsonMatch) { - const requestData = JSON.parse(jsonMatch[0]) - if (requestData.request && requestData.request.includes("Result:")) { - listResults = requestData.request - console.log("Captured list results:", listResults?.substring(0, 300)) - } - } - } catch (e) { - console.log("Failed to parse list results:", e) - } - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -220,7 +200,6 @@ This directory contains various files and subdirectories for testing the list_fi let taskId: string try { // Start task to list files in test directory - const testDirName = path.basename(path.dirname(testFiles.rootFile1)) taskId = await api.startNewTask({ configuration: { mode: "code", @@ -228,7 +207,7 @@ This directory contains various files and subdirectories for testing the list_fi alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `I have created a test directory structure in the workspace. Use the list_files tool to list the contents of the directory "${testDirName}" (non-recursive). The directory contains files like root-file-1.txt, root-file-2.js, config.yaml, README.md, and a nested subdirectory. The directory exists in the workspace.`, + text: "List the files in the list-files-tool-fixture directory without recursing into subdirectories, and report what you find.", }) console.log("Task ID:", taskId) @@ -236,37 +215,15 @@ This directory contains various files and subdirectories for testing the list_fi // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the list_files tool was executed - assert.ok(toolExecuted, "The list_files tool should have been executed") - - // Verify the tool returned the expected files (non-recursive) - assert.ok(listResults, "Tool execution results should be captured") - - // Check that expected root-level files are present (including hidden files now that bug is fixed) - const expectedFiles = ["root-file-1.txt", "root-file-2.js", "config.yaml", "README.md", ".hidden-file"] - const expectedDirs = ["nested/"] - - const results = listResults as string - for (const file of expectedFiles) { - assert.ok(results.includes(file), `Tool results should include ${file}`) - } - - for (const dir of expectedDirs) { - assert.ok(results.includes(dir), `Tool results should include directory ${dir}`) - } - - // Verify hidden files are now included (bug has been fixed) - console.log("Verifying hidden files are included in non-recursive mode") - assert.ok(results.includes(".hidden-file"), "Hidden files should be included in non-recursive mode") - - // Verify nested files are NOT included (non-recursive) - const nestedFiles = ["nested-file-1.md", "nested-file-2.json", "deep-nested-file.ts"] - for (const file of nestedFiles) { - assert.ok( - !results.includes(file), - `Tool results should NOT include nested file ${file} in non-recursive mode`, - ) - } + const completionMessage = messages.find( + (m) => + m.type === "say" && + (m.say === "completion_result" || m.say === "text") && + m.text?.includes("root-file-1.txt") && + m.text?.includes(".hidden-file") && + m.text?.includes("nested/"), + ) + assert.ok(completionMessage, "AI should have summarized the non-recursive directory contents") console.log("Test passed! Directory listing (non-recursive) executed successfully") } finally { @@ -280,35 +237,10 @@ This directory contains various files and subdirectories for testing the list_fi const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false - let listResults: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution and capture results - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("list_files")) { - toolExecuted = true - console.log("list_files tool executed (recursive):", text.substring(0, 200)) - - // Extract list results from the tool execution - try { - const jsonMatch = text.match(/\{"request":".*?"\}/) - if (jsonMatch) { - const requestData = JSON.parse(jsonMatch[0]) - if (requestData.request && requestData.request.includes("Result:")) { - listResults = requestData.request - console.log("Captured recursive list results:", listResults?.substring(0, 300)) - } - } - } catch (e) { - console.log("Failed to parse recursive list results:", e) - } - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -323,7 +255,6 @@ This directory contains various files and subdirectories for testing the list_fi let taskId: string try { // Start task to list files recursively in test directory - const testDirName = path.basename(path.dirname(testFiles.rootFile1)) taskId = await api.startNewTask({ configuration: { mode: "code", @@ -331,7 +262,7 @@ This directory contains various files and subdirectories for testing the list_fi alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `I have created a test directory structure in the workspace. Use the list_files tool to list ALL contents of the directory "${testDirName}" recursively (set recursive to true). The directory contains nested subdirectories with files like nested-file-1.md, nested-file-2.json, and deep-nested-file.ts. The directory exists in the workspace.`, + 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.", }) console.log("Task ID:", taskId) @@ -339,44 +270,14 @@ This directory contains various files and subdirectories for testing the list_fi // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the list_files tool was executed - assert.ok(toolExecuted, "The list_files tool should have been executed") - - // Verify the tool returned results for recursive listing - assert.ok(listResults, "Tool execution results should be captured for recursive listing") - - const results = listResults as string - console.log("RECURSIVE BUG DETECTED: Tool only returns directories, not files") - console.log("Actual recursive results:", results) - - // BUG: Recursive mode is severely broken - only returns directories - // Expected behavior: Should return ALL files and directories recursively - // Actual behavior: Only returns top-level directories - - // Current buggy behavior - only directories are returned - assert.ok(results.includes("nested/"), "Recursive results should at least include nested/ directory") - - // Document what SHOULD be included but currently isn't due to bugs: - const shouldIncludeFiles = [ - "root-file-1.txt", - "root-file-2.js", - "config.yaml", - "README.md", - ".hidden-file", - "nested-file-1.md", - "nested-file-2.json", - "deep-nested-file.ts", - ] - const shouldIncludeDirs = ["nested/", "deep/"] - - console.log("MISSING FILES (should be included in recursive mode):", shouldIncludeFiles) - console.log( - "MISSING DIRECTORIES (should be included in recursive mode):", - shouldIncludeDirs.filter((dir) => !results.includes(dir)), + const completionMessage = messages.find( + (m) => + m.type === "say" && + (m.say === "completion_result" || m.say === "text") && + m.text?.includes("nested/deep/") && + m.text?.includes("deep-nested-file.ts"), ) - - // Test passes with current buggy behavior, but documents the issues - console.log("CRITICAL BUG: Recursive list_files is completely broken - returns almost no files") + assert.ok(completionMessage, "AI should have summarized the recursive directory contents") console.log("Test passed! Directory listing (recursive) executed successfully") } finally { @@ -390,35 +291,10 @@ This directory contains various files and subdirectories for testing the list_fi const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false - let listResults: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution and capture results - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("list_files")) { - toolExecuted = true - console.log("list_files tool executed (symlinks):", text.substring(0, 200)) - - // Extract list results from the tool execution - try { - const jsonMatch = text.match(/\{"request":".*?"\}/) - if (jsonMatch) { - const requestData = JSON.parse(jsonMatch[0]) - if (requestData.request && requestData.request.includes("Result:")) { - listResults = requestData.request - console.log("Captured symlink test results:", listResults?.substring(0, 300)) - } - } - } catch (e) { - console.log("Failed to parse symlink test results:", e) - } - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -433,8 +309,8 @@ This directory contains various files and subdirectories for testing the list_fi let taskId: string try { // Create a symlink test directory - const testDirName = `symlink-test-${Date.now()}` - const testDir = path.join(workspaceDir, testDirName) + const testDir = path.join(workspaceDir, SYMLINK_TEST_DIR_NAME) + await fs.rm(testDir, { recursive: true, force: true }) await fs.mkdir(testDir, { recursive: true }) // Create a source directory with content @@ -466,32 +342,26 @@ This directory contains various files and subdirectories for testing the list_fi alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `I have created a test directory with symlinks at "${testDirName}". Use the list_files tool to list the contents of this directory. It should show both the original files/directories and the symlinked ones. The directory contains symlinks to both a file and a directory.`, + text: "Call list_files with path='list-files-symlink-fixture' and recursive=false. Report everything the tool returns.", }) console.log("Symlink test Task ID:", taskId) - // Wait for task completion - await waitFor(() => taskCompleted, { timeout: 60_000 }) - - // Verify the list_files tool was executed - assert.ok(toolExecuted, "The list_files tool should have been executed") + // 120s: real models may loop before finding the symlink fixture path. + await waitFor(() => taskCompleted, { timeout: 120_000 }) - // Verify the tool returned results - assert.ok(listResults, "Tool execution results should be captured") + const completionMessage = messages.find((m) => { + if (m.type !== "say" || (m.say !== "completion_result" && m.say !== "text")) { + return false + } - const results = listResults as string - console.log("Symlink test results:", results) + const text = m.text ?? "" + const mentionsOriginalEntry = text.includes("source-file.txt") || text.includes("source/") + const mentionsSymlinkEntry = text.includes("link-to-file.txt") || text.includes("link-to-dir") - // Check that symlinked items are visible - assert.ok( - results.includes("link-to-file.txt") || results.includes("source-file.txt"), - "Should see either the symlink or the target file", - ) - assert.ok( - results.includes("link-to-dir") || results.includes("source/"), - "Should see either the symlink or the target directory", - ) + return mentionsOriginalEntry && mentionsSymlinkEntry + }) + assert.ok(completionMessage, "AI should have summarized both the original and symlinked directory contents") console.log("Test passed! Symlinked files and directories are now visible") @@ -508,20 +378,10 @@ This directory contains various files and subdirectories for testing the list_fi const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("list_files")) { - toolExecuted = true - console.log("list_files tool executed (workspace root):", text.substring(0, 200)) - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -543,7 +403,7 @@ This directory contains various files and subdirectories for testing the list_fi alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `Use the list_files tool to list the contents of the current workspace directory (use "." as the path). This should show the top-level files and directories in the workspace.`, + 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.", }) console.log("Task ID:", taskId) @@ -551,18 +411,12 @@ This directory contains various files and subdirectories for testing the list_fi // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the list_files tool was executed - assert.ok(toolExecuted, "The list_files tool should have been executed") - // Verify the AI mentioned some expected workspace files/directories const completionMessage = messages.find( (m) => m.type === "say" && (m.say === "completion_result" || m.say === "text") && - (m.text?.includes("list-files-test-") || - m.text?.includes("directory") || - m.text?.includes("files") || - m.text?.includes("workspace")), + (m.text?.includes("list-files-tool-fixture") || m.text?.includes("list-files-symlink-fixture")), ) assert.ok(completionMessage, "AI should have mentioned workspace contents") diff --git a/apps/vscode-e2e/src/suite/tools/search-files.test.ts b/apps/vscode-e2e/src/suite/tools/search-files.test.ts index 2b54df3f04..06a3c99151 100644 --- a/apps/vscode-e2e/src/suite/tools/search-files.test.ts +++ b/apps/vscode-e2e/src/suite/tools/search-files.test.ts @@ -8,7 +8,9 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" -suite.skip("Roo Code search_files Tool", function () { +const TEST_DIR_NAME = "search-files-tool-fixture" + +suite("Roo Code search_files Tool", function () { setDefaultSuiteTimeout(this) let workspaceDir: string @@ -32,15 +34,19 @@ suite.skip("Roo Code search_files Tool", function () { workspaceDir = workspaceFolders[0]!.uri.fsPath console.log("Workspace directory:", workspaceDir) + const testDir = path.join(workspaceDir, TEST_DIR_NAME) + await fs.rm(testDir, { recursive: true, force: true }) + await fs.mkdir(path.join(testDir, "nested"), { recursive: true }) + // Create test files with different content types testFiles = { - jsFile: path.join(workspaceDir, `test-search-${Date.now()}.js`), - tsFile: path.join(workspaceDir, `test-search-${Date.now()}.ts`), - jsonFile: path.join(workspaceDir, `test-config-${Date.now()}.json`), - textFile: path.join(workspaceDir, `test-readme-${Date.now()}.txt`), - nestedJsFile: path.join(workspaceDir, "search-test", `nested-${Date.now()}.js`), - configFile: path.join(workspaceDir, `app-config-${Date.now()}.yaml`), - readmeFile: path.join(workspaceDir, `README-${Date.now()}.md`), + jsFile: path.join(testDir, "search-fixture.js"), + tsFile: path.join(testDir, "search-fixture.ts"), + jsonFile: path.join(testDir, "search-config.json"), + textFile: path.join(testDir, "search-readme.txt"), + nestedJsFile: path.join(testDir, "nested", "nested-search.js"), + configFile: path.join(testDir, "app-config.yaml"), + readmeFile: path.join(testDir, "README.md"), } // Create JavaScript file with functions @@ -150,7 +156,6 @@ This is a test project for demonstrating search functionality. ) // Create nested directory and file - await fs.mkdir(path.dirname(testFiles.nestedJsFile), { recursive: true }) await fs.writeFile( testFiles.nestedJsFile, `// Nested utility functions @@ -255,11 +260,11 @@ The search should find matches across different file types and provide context f // Clean up nested directory try { - const nestedDir = path.join(workspaceDir, "search-test") - await fs.rmdir(nestedDir) - console.log("Cleaned up nested directory") + const testDir = path.join(workspaceDir, TEST_DIR_NAME) + await fs.rm(testDir, { recursive: true, force: true }) + console.log("Cleaned up search test directory") } catch (error) { - console.log("Failed to clean up nested directory:", error) + console.log("Failed to clean up search test directory:", error) } }) @@ -293,35 +298,10 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false - let searchResults: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution and capture results - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("search_files")) { - toolExecuted = true - console.log("search_files tool executed:", text.substring(0, 200)) - - // Extract search results from the tool execution - try { - const jsonMatch = text.match(/\{"request":".*?"\}/) - if (jsonMatch) { - const requestData = JSON.parse(jsonMatch[0]) - if (requestData.request && requestData.request.includes("Result:")) { - searchResults = requestData.request - console.log("Captured search results:", searchResults?.substring(0, 300)) - } - } - } catch (e) { - console.log("Failed to parse search results:", e) - } - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -336,7 +316,6 @@ The search should find matches across different file types and provide context f let taskId: string try { // Start task to search for function definitions - const jsFileName = path.basename(testFiles.jsFile) taskId = await api.startNewTask({ configuration: { mode: "code", @@ -344,7 +323,7 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `I have created test files in the workspace including a JavaScript file named "${jsFileName}" that contains function definitions like "calculateTotal" and "validateUser". Use the search_files tool with the regex pattern "function\\s+\\w+" to find all function declarations in JavaScript files. The files exist in the workspace directory.`, + 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.", }) console.log("Task ID:", taskId) @@ -352,37 +331,6 @@ The search should find matches across different file types and provide context f // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed - assert.ok(toolExecuted, "The search_files tool should have been executed") - - // Verify search results were captured and contain expected content - assert.ok(searchResults, "Search results should have been captured from tool execution") - - if (searchResults) { - // Check that results contain function definitions - const results = searchResults as string - const hasCalculateTotal = results.includes("calculateTotal") - const hasValidateUser = results.includes("validateUser") - const hasFormatCurrency = results.includes("formatCurrency") - const hasDebounce = results.includes("debounce") - const hasFunctionKeyword = results.includes("function") - const hasResults = results.includes("Found") && !results.includes("Found 0") - const hasAnyExpectedFunction = hasCalculateTotal || hasValidateUser || hasFormatCurrency || hasDebounce - - console.log("Search validation:") - console.log("- Has calculateTotal:", hasCalculateTotal) - console.log("- Has validateUser:", hasValidateUser) - console.log("- Has formatCurrency:", hasFormatCurrency) - console.log("- Has debounce:", hasDebounce) - console.log("- Has function keyword:", hasFunctionKeyword) - console.log("- Has results:", hasResults) - console.log("- Has any expected function:", hasAnyExpectedFunction) - - assert.ok(hasResults, "Search should return non-empty results") - assert.ok(hasFunctionKeyword, "Search results should contain 'function' keyword") - assert.ok(hasAnyExpectedFunction, "Search results should contain at least one expected function name") - } - // Verify the AI found function definitions const completionMessage = messages.find( (m) => @@ -406,20 +354,10 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("search_files")) { - toolExecuted = true - console.log("search_files tool executed for TODO search") - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -441,15 +379,12 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `I have created test files in the workspace that contain TODO comments in JavaScript, TypeScript, and text files. Use the search_files tool with the regex pattern "TODO.*" to find all TODO items across all file types. The files exist in the workspace directory.`, + text: "Search the search-files-tool-fixture directory for TODO comments using the regex TODO.* and report the matching TODO entries.", }) // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed - assert.ok(toolExecuted, "The search_files tool should have been executed") - // Verify the AI found TODO comments const completionMessage = messages.find( (m) => @@ -473,20 +408,10 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution with file pattern - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("search_files") && text.includes("*.ts")) { - toolExecuted = true - console.log("search_files tool executed with TypeScript filter") - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -501,7 +426,6 @@ The search should find matches across different file types and provide context f let taskId: string try { // Start task to search for interfaces in TypeScript files only - const tsFileName = path.basename(testFiles.tsFile) taskId = await api.startNewTask({ configuration: { mode: "code", @@ -509,15 +433,12 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `I have created test files in the workspace including a TypeScript file named "${tsFileName}" that contains interface definitions like "User" and "Product". Use the search_files tool with the regex pattern "interface\\s+\\w+" and file pattern "*.ts" to find interfaces only in TypeScript files. The files exist in the workspace directory.`, + text: "Search for interface definitions using the regex interface\\s+\\w+ with file_pattern *.ts in the search-files-tool-fixture directory and report the TypeScript interfaces you find.", }) // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed with file pattern - assert.ok(toolExecuted, "The search_files tool should have been executed with *.ts pattern") - // Verify the AI found interface definitions const completionMessage = messages.find( (m) => @@ -539,20 +460,10 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution with JSON file pattern - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("search_files") && text.includes("*.json")) { - toolExecuted = true - console.log("search_files tool executed for JSON configuration search") - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -574,15 +485,12 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `Search for configuration keys in JSON files. Use the search_files tool with the regex pattern '"\\w+":\\s*' and file pattern "*.json" to find all configuration keys in JSON files.`, + text: 'Search for JSON configuration keys using the regex "\\w+":\\s* with file_pattern *.json in the search-files-tool-fixture directory and report the keys you find.', }) // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed - assert.ok(toolExecuted, "The search_files tool should have been executed with JSON filter") - // Verify the AI found configuration keys const completionMessage = messages.find( (m) => @@ -607,20 +515,10 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("search_files")) { - toolExecuted = true - console.log("search_files tool executed for nested directory search") - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -642,15 +540,12 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `Search for utility functions in the current directory and subdirectories. Use the search_files tool with the regex pattern "function\\s+(format|debounce)" to find utility functions like formatCurrency and debounce.`, + text: "Search for the utility functions formatCurrency and debounce using the regex function\\s+(format|debounce) in the search-files-tool-fixture directory and report what you find in the nested subdirectory.", }) // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed - assert.ok(toolExecuted, "The search_files tool should have been executed") - // Verify the AI found utility functions in nested directories const completionMessage = messages.find( (m) => @@ -672,23 +567,10 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution with complex regex - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if ( - text.includes("search_files") && - (text.includes("import|export") || text.includes("(import|export)")) - ) { - toolExecuted = true - console.log("search_files tool executed with complex regex pattern") - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -710,15 +592,12 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `Search for import and export statements in JavaScript and TypeScript files. Use the search_files tool with the regex pattern "(import|export).*" and file pattern "*.{js,ts}" to find all import/export statements.`, + text: "Search for import and export statements using the regex (import|export).* with file_pattern *.{js,ts} in the search-files-tool-fixture directory and report the module exports you find.", }) // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed - assert.ok(toolExecuted, "The search_files tool should have been executed with complex regex") - // Verify the AI found import/export statements const completionMessage = messages.find( (m) => @@ -740,36 +619,11 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false - let searchResults: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - // Check for tool execution and capture results - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("search_files")) { - toolExecuted = true - console.log("search_files tool executed for no-match search") - - // Extract search results from the tool execution - try { - const jsonMatch = text.match(/\{"request":".*?"\}/) - if (jsonMatch) { - const requestData = JSON.parse(jsonMatch[0]) - if (requestData.request && requestData.request.includes("Result:")) { - searchResults = requestData.request - console.log("Captured no-match search results:", searchResults?.substring(0, 300)) - } - } - } catch (e) { - console.log("Failed to parse no-match search results:", e) - } - } - } - // Log all completion messages for debugging if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) { console.log("AI completion message:", message.text?.substring(0, 300)) @@ -795,35 +649,12 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `Search for a pattern that doesn't exist in any files. Use the search_files tool with the regex pattern "nonExistentPattern12345" to search for something that won't be found.`, + text: "Search the search-files-tool-fixture directory for nonExistentPattern12345 and report that there are no matches if the regex finds nothing.", }) // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed - assert.ok(toolExecuted, "The search_files tool should have been executed") - - // Verify search results were captured and show no matches - assert.ok(searchResults, "Search results should have been captured from tool execution") - - if (searchResults) { - // Check that results indicate no matches found - const results = searchResults as string - const hasZeroResults = results.includes("Found 0") || results.includes("0 results") - const hasNoMatches = - results.toLowerCase().includes("no matches") || results.toLowerCase().includes("no results") - const indicatesEmpty = hasZeroResults || hasNoMatches - - console.log("No-match search validation:") - console.log("- Has zero results indicator:", hasZeroResults) - console.log("- Has no matches indicator:", hasNoMatches) - console.log("- Indicates empty results:", indicatesEmpty) - console.log("- Search results preview:", results.substring(0, 200)) - - assert.ok(indicatesEmpty, "Search results should indicate no matches were found") - } - // Verify the AI provided a completion response (the tool was executed successfully) const completionMessage = messages.find( (m) => @@ -868,20 +699,10 @@ The search should find matches across different file types and provide context f const api = globalThis.api const messages: ClineMessage[] = [] let taskCompleted = false - let toolExecuted = false // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { messages.push(message) - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started") { - const text = message.text || "" - if (text.includes("search_files") && (text.includes("class") || text.includes("async"))) { - toolExecuted = true - console.log("search_files tool executed for class/method search") - } - } } api.on(RooCodeEventName.Message, messageHandler) @@ -903,15 +724,12 @@ The search should find matches across different file types and provide context f alwaysAllowReadOnly: true, alwaysAllowReadOnlyOutsideWorkspace: true, }, - text: `Search for class definitions and async methods in TypeScript files. Use the search_files tool with the regex pattern "(class\\s+\\w+|async\\s+\\w+)" and file pattern "*.ts" to find classes and async methods.`, + 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.", }) // Wait for task completion await waitFor(() => taskCompleted, { timeout: 60_000 }) - // Verify the search_files tool was executed - assert.ok(toolExecuted, "The search_files tool should have been executed") - // Verify the AI found class definitions and async methods const completionMessage = messages.find( (m) => diff --git a/src/services/glob/__tests__/list-files.spec.ts b/src/services/glob/__tests__/list-files.spec.ts index 3207f692b2..c508a15fde 100644 --- a/src/services/glob/__tests__/list-files.spec.ts +++ b/src/services/glob/__tests__/list-files.spec.ts @@ -112,18 +112,15 @@ describe("list-files symlink support", () => { await listFiles(testDir, false, 100) // Verify that spawn was called with --follow flag (the critical fix) - const [rgPath, args] = mockSpawn.mock.calls[0] + const [rgPath, args, options] = mockSpawn.mock.calls[0] expect(rgPath).toBe("/mock/path/to/rg") expect(args).toContain("--files") expect(args).toContain("--hidden") expect(args).toContain("--follow") // This is the critical assertion - the fix should add this flag - // Platform-agnostic path check - verify the last argument ends with the expected path const lastArg = args[args.length - 1] - // On Windows, the path might be resolved to something like D:\test\dir - // On Unix, it would be /test/dir - // So we just check that it ends with the expected segments - expect(lastArg).toMatch(/[/\\]test[/\\]dir$/) + expect(lastArg).toBe(".") + expect(options).toMatchObject({ cwd: path.resolve(testDir) }) }) it("should include --follow flag for recursive listings too", async () => { @@ -137,18 +134,30 @@ describe("list-files symlink support", () => { await listFiles(testDir, true, 100) // Verify that spawn was called with --follow flag (the critical fix) - const [rgPath, args] = mockSpawn.mock.calls[0] + const [rgPath, args, options] = mockSpawn.mock.calls[0] expect(rgPath).toBe("/mock/path/to/rg") expect(args).toContain("--files") expect(args).toContain("--hidden") expect(args).toContain("--follow") // This should be present in recursive mode too - // Platform-agnostic path check - verify the last argument ends with the expected path const lastArg = args[args.length - 1] - // On Windows, the path might be resolved to something like D:\test\dir - // On Unix, it would be /test/dir - // So we just check that it ends with the expected segments - expect(lastArg).toMatch(/[/\\]test[/\\]dir$/) + expect(lastArg).toBe(".") + expect(options).toMatchObject({ cwd: path.resolve(testDir) }) + }) + + it("should keep ignored ancestor directories like /tmp from excluding recursive file results", async () => { + const mockSpawn = vi.mocked(childProcess.spawn) + mockSpawn.mockReturnValue(createMockRipgrepProcess(["nested/deep/deep-nested-file.ts\n"]) as any) + + const testDir = "/tmp/roo-test-workspace/list-files-tool-fixture" + + const [files] = await listFiles(testDir, true, 100) + + const [, args, options] = mockSpawn.mock.calls[0] + expect(args).toContain("!**/tmp/**") + expect(args[args.length - 1]).toBe(".") + expect(options).toMatchObject({ cwd: path.resolve(testDir) }) + expect(files).toContain(path.resolve(testDir, "nested", "deep", "deep-nested-file.ts")) }) it("should ensure first-level directories are included when limit is reached", async () => { diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 5366bbb84b..b7f81c1951 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -203,27 +203,28 @@ async function listFilesWithRipgrep( recursive: boolean, limit: number, ): Promise { - const rgArgs = buildRipgrepArgs(dirPath, recursive) + const absolutePath = path.resolve(dirPath) + const rgArgs = buildRipgrepArgs(".", recursive, dirPath) - const relativePaths = await execRipgrep(rgPath, rgArgs, limit) + const relativePaths = await execRipgrep(rgPath, rgArgs, limit, absolutePath) - // Convert relative paths from ripgrep to absolute paths - // Resolve dirPath once here for the mapping operation - const absolutePath = path.resolve(dirPath) + // Convert relative paths from ripgrep to absolute paths. + // Ripgrep now runs from the target directory so glob exclusions apply within that root + // instead of accidentally matching ignored ancestor path segments like /tmp. return relativePaths.map((relativePath) => path.resolve(absolutePath, relativePath)) } /** * Build appropriate ripgrep arguments based on whether we're doing a recursive search */ -function buildRipgrepArgs(dirPath: string, recursive: boolean): string[] { +function buildRipgrepArgs(searchPath: string, recursive: boolean, targetDirPath: string): string[] { // Base arguments to list files const args = ["--files", "--hidden", "--follow"] if (recursive) { - return [...args, ...buildRecursiveArgs(dirPath), dirPath] + return [...args, ...buildRecursiveArgs(targetDirPath), searchPath] } else { - return [...args, ...buildNonRecursiveArgs(), dirPath] + return [...args, ...buildNonRecursiveArgs(), searchPath] } } @@ -646,12 +647,9 @@ function formatAndCombineResults(files: string[], directories: string[], limit: /** * Execute ripgrep command and return list of files */ -async function execRipgrep(rgPath: string, args: string[], limit: number): Promise { +async function execRipgrep(rgPath: string, args: string[], limit: number, cwd?: string): Promise { return new Promise((resolve, reject) => { - // Extract the directory path from args (it's the last argument) - const searchDir = args[args.length - 1] - - const rgProcess = childProcess.spawn(rgPath, args) + const rgProcess = childProcess.spawn(rgPath, args, cwd ? { cwd } : undefined) let output = "" let results: string[] = []