diff --git a/src/core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts new file mode 100644 index 00000000000..72ce82da8e3 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts @@ -0,0 +1,233 @@ +// npx vitest core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts + +import type { Mock } from "vitest" + +// Mock dependencies - must come before imports +vi.mock("../../../services/search/file-search") +vi.mock("../../ignore/RooIgnoreController") + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" +import { searchWorkspaceFiles } from "../../../services/search/file-search" +import { RooIgnoreController } from "../../ignore/RooIgnoreController" + +const mockSearchWorkspaceFiles = searchWorkspaceFiles as Mock + +vi.mock("vscode", () => ({ + window: { + showInformationMessage: vi.fn(), + showErrorMessage: vi.fn(), + }, + workspace: { + workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }], + }, +})) + +describe("webviewMessageHandler - searchFiles with RooIgnore filtering", () => { + let mockClineProvider: ClineProvider + let mockFilterPaths: Mock + + beforeEach(() => { + vi.clearAllMocks() + + // Spy on the mock RooIgnoreController prototype methods + mockFilterPaths = vi.fn() + + // Override the filterPaths method on the prototype + ;(RooIgnoreController.prototype as any).filterPaths = mockFilterPaths + ;(RooIgnoreController.prototype as any).initialize = vi.fn().mockResolvedValue(undefined) + + // Create mock ClineProvider + mockClineProvider = { + getState: vi.fn(), + postMessageToWebview: vi.fn(), + getCurrentTask: vi.fn(), + cwd: "/mock/workspace", + } as unknown as ClineProvider + }) + + it("should filter results using RooIgnoreController when showRooIgnoredFiles is false", async () => { + // Setup mock results from file search + const mockResults = [ + { path: "src/index.ts", type: "file" as const, label: "index.ts" }, + { path: "secrets/config.json", type: "file" as const, label: "config.json" }, + { path: "src/utils.ts", type: "file" as const, label: "utils.ts" }, + ] + mockSearchWorkspaceFiles.mockResolvedValue(mockResults) + + // Setup state with showRooIgnoredFiles = false + ;(mockClineProvider.getState as Mock).mockResolvedValue({ + showRooIgnoredFiles: false, + }) + + // Setup filter to exclude secrets folder + mockFilterPaths.mockReturnValue(["src/index.ts", "src/utils.ts"]) + + // No current task, so temporary controller will be created + ;(mockClineProvider.getCurrentTask as Mock).mockReturnValue(null) + + await webviewMessageHandler(mockClineProvider, { + type: "searchFiles", + query: "index", + requestId: "test-request-123", + }) + + // Verify filterPaths was called with all result paths + expect(mockFilterPaths).toHaveBeenCalledWith(["src/index.ts", "secrets/config.json", "src/utils.ts"]) + + // Verify filtered results were sent to webview + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "fileSearchResults", + results: [ + { path: "src/index.ts", type: "file", label: "index.ts" }, + { path: "src/utils.ts", type: "file", label: "utils.ts" }, + ], + requestId: "test-request-123", + }) + }) + + it("should not filter results when showRooIgnoredFiles is true", async () => { + // Setup mock results from file search + const mockResults = [ + { path: "src/index.ts", type: "file" as const, label: "index.ts" }, + { path: "secrets/config.json", type: "file" as const, label: "config.json" }, + ] + mockSearchWorkspaceFiles.mockResolvedValue(mockResults) + + // Setup state with showRooIgnoredFiles = true + ;(mockClineProvider.getState as Mock).mockResolvedValue({ + showRooIgnoredFiles: true, + }) + + // No current task + ;(mockClineProvider.getCurrentTask as Mock).mockReturnValue(null) + + await webviewMessageHandler(mockClineProvider, { + type: "searchFiles", + query: "index", + requestId: "test-request-456", + }) + + // Verify filterPaths was NOT called + expect(mockFilterPaths).not.toHaveBeenCalled() + + // Verify all results were sent to webview (unfiltered) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "fileSearchResults", + results: mockResults, + requestId: "test-request-456", + }) + }) + + it("should use existing RooIgnoreController from current task", async () => { + // Setup mock results from file search + const mockResults = [ + { path: "src/index.ts", type: "file" as const, label: "index.ts" }, + { path: "private/secret.ts", type: "file" as const, label: "secret.ts" }, + ] + mockSearchWorkspaceFiles.mockResolvedValue(mockResults) + + // Setup state with showRooIgnoredFiles = false + ;(mockClineProvider.getState as Mock).mockResolvedValue({ + showRooIgnoredFiles: false, + }) + + // Create a mock task with its own RooIgnoreController + const taskFilterPaths = vi.fn().mockReturnValue(["src/index.ts"]) + const taskRooIgnoreController = { + filterPaths: taskFilterPaths, + initialize: vi.fn(), + } + ;(mockClineProvider.getCurrentTask as Mock).mockReturnValue({ + taskId: "test-task-id", + rooIgnoreController: taskRooIgnoreController, + }) + + await webviewMessageHandler(mockClineProvider, { + type: "searchFiles", + query: "index", + requestId: "test-request-789", + }) + + // Verify the task's controller was used (not the prototype) + expect(taskFilterPaths).toHaveBeenCalledWith(["src/index.ts", "private/secret.ts"]) + + // Verify filtered results were sent to webview + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "fileSearchResults", + results: [{ path: "src/index.ts", type: "file", label: "index.ts" }], + requestId: "test-request-789", + }) + }) + + it("should handle error when no workspace path is available", async () => { + // Create provider without cwd + mockClineProvider = { + ...mockClineProvider, + cwd: undefined, + getCurrentTask: vi.fn().mockReturnValue(null), + } as unknown as ClineProvider + + await webviewMessageHandler(mockClineProvider, { + type: "searchFiles", + query: "test", + requestId: "test-request-error", + }) + + // Verify error response was sent + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "fileSearchResults", + results: [], + requestId: "test-request-error", + error: "No workspace path available", + }) + }) + + it("should handle errors from searchWorkspaceFiles", async () => { + mockSearchWorkspaceFiles.mockRejectedValue(new Error("File search failed")) + + // Setup state + ;(mockClineProvider.getState as Mock).mockResolvedValue({ + showRooIgnoredFiles: false, + }) + ;(mockClineProvider.getCurrentTask as Mock).mockReturnValue(null) + + await webviewMessageHandler(mockClineProvider, { + type: "searchFiles", + query: "test", + requestId: "test-request-fail", + }) + + // Verify error response was sent + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "fileSearchResults", + results: [], + error: "File search failed", + requestId: "test-request-fail", + }) + }) + + it("should default showRooIgnoredFiles to false when state is null", async () => { + // Setup mock results from file search + const mockResults = [{ path: "src/index.ts", type: "file" as const, label: "index.ts" }] + mockSearchWorkspaceFiles.mockResolvedValue(mockResults) + + // Setup state to return null + ;(mockClineProvider.getState as Mock).mockResolvedValue(null) + + // Setup filter to return all paths (no filtering) + mockFilterPaths.mockReturnValue(["src/index.ts"]) + + // No current task + ;(mockClineProvider.getCurrentTask as Mock).mockReturnValue(null) + + await webviewMessageHandler(mockClineProvider, { + type: "searchFiles", + query: "index", + requestId: "test-request-default", + }) + + // Verify filterPaths was called (showRooIgnoredFiles defaults to false) + expect(mockFilterPaths).toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e1640d3f2a8..c3d0de303ee 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -52,6 +52,7 @@ import { exportSettings, importSettingsWithFeedback } from "../config/importExpo import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" +import { RooIgnoreController } from "../ignore/RooIgnoreController" import { getWorkspacePath } from "../../utils/path" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" @@ -1692,10 +1693,30 @@ export const webviewMessageHandler = async ( 20, // Use default limit, as filtering is now done in the backend ) + // Get the RooIgnoreController from the current task, or create a new one + const currentTask = provider.getCurrentTask() + let rooIgnoreController = currentTask?.rooIgnoreController + + // If no current task or no controller, create a temporary one + if (!rooIgnoreController) { + rooIgnoreController = new RooIgnoreController(workspacePath) + await rooIgnoreController.initialize() + } + + // Get showRooIgnoredFiles setting from state + const { showRooIgnoredFiles = false } = (await provider.getState()) ?? {} + + // Filter results using RooIgnoreController if showRooIgnoredFiles is false + let filteredResults = results + if (!showRooIgnoredFiles && rooIgnoreController) { + const allowedPaths = rooIgnoreController.filterPaths(results.map((r) => r.path)) + filteredResults = results.filter((r) => allowedPaths.includes(r.path)) + } + // Send results back to webview await provider.postMessageToWebview({ type: "fileSearchResults", - results, + results: filteredResults, requestId: message.requestId, }) } catch (error) {