/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { canUseRipgrep, RipGrepTool, ensureRgPath, getRipgrepPath, } from './ripGrep.js';
import path from 'node:path';
import { isSubpath } from '../utils/paths.js';
import fs from 'node:fs/promises';
import os from 'node:os';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { spawn } from 'node:child_process';
import { PassThrough, Readable } from 'node:stream';
import EventEmitter from 'node:events';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { fileExists } from '../utils/fileUtils.js';
vi.mock('../utils/fileUtils.js', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        fileExists: vi.fn(),
    };
});
// Mock child_process for ripgrep calls
vi.mock('child_process', () => ({
    spawn: vi.fn(),
}));
const mockSpawn = vi.mocked(spawn);
describe('canUseRipgrep', () => {
    beforeEach(() => {
        vi.mocked(fileExists).mockReset();
    });
    it('should return true if ripgrep already exists', async () => {
        vi.mocked(fileExists).mockResolvedValue(true);
        const result = await canUseRipgrep();
        expect(result).toBe(true);
    });
    it('should return false if file does not exist', async () => {
        vi.mocked(fileExists).mockResolvedValue(false);
        const result = await canUseRipgrep();
        expect(result).toBe(false);
    });
});
describe('ensureRgPath', () => {
    beforeEach(() => {
        vi.mocked(fileExists).mockReset();
    });
    it('should return rg path if ripgrep already exists', async () => {
        vi.mocked(fileExists).mockResolvedValue(true);
        const rgPath = await ensureRgPath();
        expect(rgPath).toBe(await getRipgrepPath());
    });
    it('should throw an error if ripgrep cannot be used', async () => {
        vi.mocked(fileExists).mockResolvedValue(false);
        await expect(ensureRgPath()).rejects.toThrow(/Cannot find bundled ripgrep binary/);
    });
});
// Helper function to create mock spawn implementations
function createMockSpawn(options = {}) {
    const { outputData, exitCode = 0, signal } = options;
    return () => {
        // strict Readable implementation
        let pushed = false;
        const stdout = new Readable({
            read() {
                if (!pushed) {
                    if (outputData) {
                        this.push(outputData);
                    }
                    this.push(null); // EOF
                    pushed = true;
                }
            },
        });
        const stderr = new PassThrough();
        const mockProcess = new EventEmitter();
        mockProcess.stdout = stdout;
        mockProcess.stderr = stderr;
        mockProcess.kill = vi.fn();
        // @ts-expect-error - mocking private/internal property
        mockProcess.killed = false;
        // @ts-expect-error - mocking private/internal property
        mockProcess.exitCode = null;
        // Emulating process exit
        setTimeout(() => {
            mockProcess.emit('close', exitCode, signal);
        }, 10);
        return mockProcess;
    };
}
describe('RipGrepTool', () => {
    let tempRootDir;
    let grepTool;
    const abortSignal = new AbortController().signal;
    let mockConfig = {
        getTargetDir: () => tempRootDir,
        getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
        getDebugMode: () => false,
        getFileFilteringRespectGitIgnore: () => true,
        getFileFilteringRespectGeminiIgnore: () => true,
        getFileFilteringOptions: () => ({
            respectGitIgnore: true,
            respectGeminiIgnore: true,
        }),
    };
    beforeEach(async () => {
        mockSpawn.mockReset();
        mockSpawn.mockImplementation(createMockSpawn());
        tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
        vi.mocked(fileExists).mockResolvedValue(true);
        mockConfig = {
            getTargetDir: () => tempRootDir,
            getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
            getDebugMode: () => false,
            getFileFilteringRespectGitIgnore: () => true,
            getFileFilteringRespectGeminiIgnore: () => true,
            getFileFilteringOptions: () => ({
                respectGitIgnore: true,
                respectGeminiIgnore: true,
            }),
            storage: {
                getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
            },
            isPathAllowed(absolutePath) {
                const workspaceContext = this.getWorkspaceContext();
                if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
                    return true;
                }
                const projectTempDir = this.storage.getProjectTempDir();
                return isSubpath(path.resolve(projectTempDir), absolutePath);
            },
            validatePathAccess(absolutePath) {
                if (this.isPathAllowed(absolutePath)) {
                    return null;
                }
                const workspaceDirs = this.getWorkspaceContext().getDirectories();
                const projectTempDir = this.storage.getProjectTempDir();
                return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
            },
        };
        grepTool = new RipGrepTool(mockConfig, createMockMessageBus());
        // Create some test files and directories
        await fs.writeFile(path.join(tempRootDir, 'fileA.txt'), 'hello world\nsecond line with world');
        await fs.writeFile(path.join(tempRootDir, 'fileB.js'), 'const foo = "bar";\nfunction baz() { return "hello"; }');
        await fs.mkdir(path.join(tempRootDir, 'sub'));
        await fs.writeFile(path.join(tempRootDir, 'sub', 'fileC.txt'), 'another world in sub dir');
        await fs.writeFile(path.join(tempRootDir, 'sub', 'fileD.md'), '# Markdown file\nThis is a test.');
    });
    afterEach(async () => {
        await fs.rm(tempRootDir, { recursive: true, force: true });
    });
    describe('validateToolParams', () => {
        it.each([
            {
                name: 'pattern only',
                params: { pattern: 'hello' },
                expected: null,
            },
            {
                name: 'pattern and path',
                params: { pattern: 'hello', dir_path: '.' },
                expected: null,
            },
            {
                name: 'pattern, path, and include',
                params: { pattern: 'hello', dir_path: '.', include_pattern: '*.txt' },
                expected: null,
            },
        ])('should return null for valid params ($name)', ({ params, expected }) => {
            expect(grepTool.validateToolParams(params)).toBe(expected);
        });
        it('should throw error for invalid regex pattern', () => {
            const params = { pattern: '[[' };
            expect(grepTool.validateToolParams(params)).toMatch(/Invalid regular expression pattern provided/);
        });
        it('should return error if pattern is missing', () => {
            const params = { dir_path: '.' };
            expect(grepTool.validateToolParams(params)).toBe(`params must have required property 'pattern'`);
        });
        it('should return error if path does not exist', () => {
            const params = {
                pattern: 'hello',
                dir_path: 'nonexistent',
            };
            // Check for the core error message, as the full path might vary
            const result = grepTool.validateToolParams(params);
            expect(result).toMatch(/Path does not exist/);
            expect(result).toMatch(/nonexistent/);
        });
        it('should allow path to be a file', async () => {
            const filePath = path.join(tempRootDir, 'fileA.txt');
            const params = {
                pattern: 'hello',
                dir_path: filePath,
            };
            expect(grepTool.validateToolParams(params)).toBeNull();
        });
    });
    describe('execute', () => {
        it('should find matches for a simple pattern in all files', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello world\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileA.txt' },
                            line_number: 2,
                            lines: { text: 'second line with world\n' },
                        },
                    }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'sub/fileC.txt' },
                            line_number: 1,
                            lines: { text: 'another world in sub dir\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'world' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 3 matches for pattern "world" in path "."');
            expect(result.llmContent).toContain('File: fileA.txt');
            expect(result.llmContent).toContain('L1: hello world');
            expect(result.llmContent).toContain('L2: second line with world');
            expect(result.llmContent).toContain(`File: ${path.join('sub', 'fileC.txt')}`);
            expect(result.llmContent).toContain('L1: another world in sub dir');
            expect(result.returnDisplay.summary).toBe('Found 3 matches');
        });
        it('should ignore matches that escape the base path', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: '..env' },
                        line_number: 1,
                        lines: { text: 'world in ..env\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: '../secret.txt' },
                            line_number: 1,
                            lines: { text: 'leak\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'world' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('File: ..env');
            expect(result.llmContent).toContain('L1: world in ..env');
            expect(result.llmContent).not.toContain('secret.txt');
        });
        it('should find matches in a specific path', async () => {
            // Setup specific mock for this test - searching in 'sub' should only return matches from that directory
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileC.txt' },
                        line_number: 1,
                        lines: { text: 'another world in sub dir\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'world', dir_path: 'sub' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 1 match for pattern "world" in path "sub"');
            expect(result.llmContent).toContain('File: fileC.txt'); // Path relative to 'sub'
            expect(result.llmContent).toContain('L1: another world in sub dir');
            expect(result.returnDisplay.summary).toBe('Found 1 match');
        });
        it('should find matches with an include glob', async () => {
            // Setup specific mock for this test
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileB.js' },
                        line_number: 2,
                        lines: { text: 'function baz() { return "hello"; }\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = {
                pattern: 'hello',
                include_pattern: '*.js',
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 1 match for pattern "hello" in path "." (filter: "*.js"):');
            expect(result.llmContent).toContain('File: fileB.js');
            expect(result.llmContent).toContain('L2: function baz() { return "hello"; }');
            expect(result.returnDisplay.summary).toBe('Found 1 match');
        });
        it('should find matches with an include glob and path', async () => {
            await fs.writeFile(path.join(tempRootDir, 'sub', 'another.js'), 'const greeting = "hello";');
            // Setup specific mock for this test - searching for 'hello' in 'sub' with '*.js' filter
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'another.js' },
                        line_number: 1,
                        lines: { text: 'const greeting = "hello";\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = {
                pattern: 'hello',
                dir_path: 'sub',
                include_pattern: '*.js',
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 1 match for pattern "hello" in path "sub" (filter: "*.js")');
            expect(result.llmContent).toContain('File: another.js');
            expect(result.llmContent).toContain('L1: const greeting = "hello";');
            expect(result.returnDisplay.summary).toBe('Found 1 match');
        });
        it('should return "No matches found" when pattern does not exist', async () => {
            // Setup specific mock for no matches
            mockSpawn.mockImplementation(createMockSpawn({
                exitCode: 1, // No matches found
            }));
            const params = { pattern: 'nonexistentpattern' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('No matches found for pattern "nonexistentpattern" in path ".".');
            expect(result.returnDisplay.summary).toBe('No matches found');
        });
        it('should throw error for invalid regex pattern during build', async () => {
            const params = { pattern: '[[' };
            expect(() => grepTool.build(params)).toThrow(/Invalid regular expression pattern provided/);
        });
        it('should ignore invalid regex error from ripgrep when it is not a user error', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: '',
                exitCode: 2,
                signal: undefined,
            }));
            const invocation = grepTool.build({
                pattern: 'foo',
                dir_path: tempRootDir,
            });
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Process exited with code 2');
            expect(result.returnDisplay).toContain('Error: Process exited with code 2');
        });
        it('should handle massive output by terminating early without crashing (Regression)', async () => {
            const massiveOutputLines = 30000;
            // Custom mock for massive streaming
            mockSpawn.mockImplementation(() => {
                const stdout = new PassThrough();
                const stderr = new PassThrough();
                const mockProcess = new EventEmitter();
                mockProcess.stdout = stdout;
                mockProcess.stderr = stderr;
                mockProcess.kill = vi.fn();
                // @ts-expect-error - mocking private/internal property
                mockProcess.killed = false;
                // @ts-expect-error - mocking private/internal property
                mockProcess.exitCode = null;
                // Push data over time
                let linesPushed = 0;
                const pushInterval = setInterval(() => {
                    if (linesPushed >= massiveOutputLines) {
                        clearInterval(pushInterval);
                        stdout.end();
                        mockProcess.emit('close', 0);
                        return;
                    }
                    // Push a batch
                    try {
                        for (let i = 0; i < 2000 && linesPushed < massiveOutputLines; i++) {
                            const match = JSON.stringify({
                                type: 'match',
                                data: {
                                    path: { text: `file_${linesPushed}.txt` },
                                    line_number: 1,
                                    lines: { text: `match ${linesPushed}\n` },
                                },
                            });
                            stdout.write(match + '\n');
                            linesPushed++;
                        }
                    }
                    catch {
                        clearInterval(pushInterval);
                    }
                }, 1);
                mockProcess.kill = vi.fn().mockImplementation(() => {
                    clearInterval(pushInterval);
                    stdout.end();
                    // Emit close async to allow listeners to attach
                    setTimeout(() => mockProcess.emit('close', 0, 'SIGTERM'), 0);
                    return true;
                });
                return mockProcess;
            });
            const invocation = grepTool.build({
                pattern: 'test',
                dir_path: tempRootDir,
            });
            const result = await invocation.execute({ abortSignal });
            expect(result.returnDisplay.summary).toContain('(limited)');
        }, 10000);
        it('should filter out files based on FileDiscoveryService even if ripgrep returns them', async () => {
            // Create .geminiignore to ignore 'ignored.txt'
            await fs.writeFile(path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME), 'ignored.txt');
            // Re-initialize tool so FileDiscoveryService loads the new .geminiignore
            const toolWithIgnore = new RipGrepTool(mockConfig, createMockMessageBus());
            // Mock ripgrep returning both an ignored file and an allowed file
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'ignored.txt' },
                        line_number: 1,
                        lines: { text: 'should be ignored\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'allowed.txt' },
                            line_number: 1,
                            lines: { text: 'should be kept\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'should' };
            const invocation = toolWithIgnore.build(params);
            const result = await invocation.execute({ abortSignal });
            // Verify ignored file is filtered out
            expect(result.llmContent).toContain('allowed.txt');
            expect(result.llmContent).toContain('should be kept');
            expect(result.llmContent).not.toContain('ignored.txt');
            expect(result.llmContent).not.toContain('should be ignored');
            expect(result.returnDisplay.summary).toContain('Found 1 match');
        });
        it('should handle regex special characters correctly', async () => {
            // Setup specific mock for this test - regex pattern 'foo.*bar' should match 'const foo = "bar";'
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileB.js' },
                        line_number: 1,
                        lines: { text: 'const foo = "bar";\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'foo.*bar' }; // Matches 'const foo = "bar";'
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 1 match for pattern "foo.*bar" in path ".":');
            expect(result.llmContent).toContain('File: fileB.js');
            expect(result.llmContent).toContain('L1: const foo = "bar";');
        });
        it('should be case-insensitive by default (JS fallback)', async () => {
            // Setup specific mock for this test - case insensitive search for 'HELLO'
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello world\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileB.js' },
                            line_number: 2,
                            lines: { text: 'function baz() { return "hello"; }\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'HELLO' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 2 matches for pattern "HELLO" in path ".":');
            expect(result.llmContent).toContain('File: fileA.txt');
            expect(result.llmContent).toContain('L1: hello world');
            expect(result.llmContent).toContain('File: fileB.js');
            expect(result.llmContent).toContain('L2: function baz() { return "hello"; }');
        });
        it('should throw an error if params are invalid', async () => {
            const params = { dir_path: '.' }; // Invalid: pattern missing
            expect(() => grepTool.build(params)).toThrow(/params must have required property 'pattern'/);
        });
        it('should throw an error if ripgrep is not available', async () => {
            vi.mocked(fileExists).mockResolvedValue(false);
            const params = { pattern: 'world' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Cannot find bundled ripgrep binary');
            // restore the mock for subsequent tests
            vi.mocked(fileExists).mockResolvedValue(true);
        });
    });
    describe('multi-directory workspace', () => {
        it('should search only CWD when no path is specified (default behavior)', async () => {
            // Create additional directory with test files
            const secondDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-second-'));
            await fs.writeFile(path.join(secondDir, 'other.txt'), 'hello from second directory\nworld in second');
            await fs.writeFile(path.join(secondDir, 'another.js'), 'function world() { return "test"; }');
            // Create a mock config with multiple directories
            const multiDirConfig = {
                getTargetDir: () => tempRootDir,
                getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir, [secondDir]),
                getDebugMode: () => false,
                getFileFilteringRespectGitIgnore: () => true,
                getFileFilteringRespectGeminiIgnore: () => true,
                getFileFilteringOptions: () => ({
                    respectGitIgnore: true,
                    respectGeminiIgnore: true,
                }),
                storage: {
                    getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
                },
                isPathAllowed(absolutePath) {
                    const workspaceContext = this.getWorkspaceContext();
                    if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
                        return true;
                    }
                    const projectTempDir = this.storage.getProjectTempDir();
                    return isSubpath(path.resolve(projectTempDir), absolutePath);
                },
                validatePathAccess(absolutePath) {
                    if (this.isPathAllowed(absolutePath)) {
                        return null;
                    }
                    const workspaceDirs = this.getWorkspaceContext().getDirectories();
                    const projectTempDir = this.storage.getProjectTempDir();
                    return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
                },
            };
            // Setup specific mock for this test - multi-directory search for 'world'
            // Mock will be called twice - once for each directory
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello world\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileA.txt' },
                            line_number: 2,
                            lines: { text: 'second line with world\n' },
                        },
                    }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'sub/fileC.txt' },
                            line_number: 1,
                            lines: { text: 'another world in sub dir\n' },
                        },
                    }) +
                    '\n',
            }));
            const multiDirGrepTool = new RipGrepTool(multiDirConfig, createMockMessageBus());
            const params = { pattern: 'world' };
            const invocation = multiDirGrepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            // Should find matches in CWD only (default behavior now)
            expect(result.llmContent).toContain('Found 3 matches for pattern "world" in path "."');
            // Matches from first directory
            expect(result.llmContent).toContain('fileA.txt');
            expect(result.llmContent).toContain('L1: hello world');
            expect(result.llmContent).toContain('L2: second line with world');
            expect(result.llmContent).toContain('fileC.txt');
            expect(result.llmContent).toContain('L1: another world in sub dir');
            // Should NOT find matches from second directory
            expect(result.llmContent).not.toContain('other.txt');
            expect(result.llmContent).not.toContain('world in second');
            expect(result.llmContent).not.toContain('another.js');
            expect(result.llmContent).not.toContain('function world()');
            // Clean up
            await fs.rm(secondDir, { recursive: true, force: true });
            mockSpawn.mockClear();
        });
        it('should search only specified path within workspace directories', async () => {
            // Create additional directory
            const secondDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-second-'));
            await fs.mkdir(path.join(secondDir, 'sub'));
            await fs.writeFile(path.join(secondDir, 'sub', 'test.txt'), 'hello from second sub directory');
            // Create a mock config with multiple directories
            const multiDirConfig = {
                getTargetDir: () => tempRootDir,
                getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir, [secondDir]),
                getDebugMode: () => false,
                getFileFilteringRespectGitIgnore: () => true,
                getFileFilteringRespectGeminiIgnore: () => true,
                getFileFilteringOptions: () => ({
                    respectGitIgnore: true,
                    respectGeminiIgnore: true,
                }),
                storage: {
                    getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
                },
                isPathAllowed(absolutePath) {
                    const workspaceContext = this.getWorkspaceContext();
                    if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
                        return true;
                    }
                    const projectTempDir = this.storage.getProjectTempDir();
                    return isSubpath(path.resolve(projectTempDir), absolutePath);
                },
                validatePathAccess(absolutePath) {
                    if (this.isPathAllowed(absolutePath)) {
                        return null;
                    }
                    const workspaceDirs = this.getWorkspaceContext().getDirectories();
                    const projectTempDir = this.storage.getProjectTempDir();
                    return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
                },
            };
            // Setup specific mock for this test - searching in 'sub' should only return matches from that directory
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileC.txt' },
                        line_number: 1,
                        lines: { text: 'another world in sub dir\n' },
                    },
                }) + '\n',
            }));
            const multiDirGrepTool = new RipGrepTool(multiDirConfig, createMockMessageBus());
            // Search only in the 'sub' directory of the first workspace
            const params = { pattern: 'world', dir_path: 'sub' };
            const invocation = multiDirGrepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            // Should only find matches in the specified sub directory
            expect(result.llmContent).toContain('Found 1 match for pattern "world" in path "sub"');
            expect(result.llmContent).toContain('File: fileC.txt');
            expect(result.llmContent).toContain('L1: another world in sub dir');
            // Should not contain matches from second directory
            expect(result.llmContent).not.toContain('test.txt');
            // Clean up
            await fs.rm(secondDir, { recursive: true, force: true });
        });
    });
    describe('abort signal handling', () => {
        it('should handle AbortSignal during search', async () => {
            const controller = new AbortController();
            const params = { pattern: 'world' };
            const invocation = grepTool.build(params);
            controller.abort();
            const result = await invocation.execute({
                abortSignal: controller.signal,
            });
            expect(result).toBeDefined();
        });
        it('should abort streaming search when signal is triggered', async () => {
            // Setup specific mock for this test - simulate process being killed due to abort
            mockSpawn.mockImplementation(createMockSpawn({
                exitCode: null,
                signal: 'SIGTERM',
            }));
            const controller = new AbortController();
            const params = { pattern: 'test' };
            const invocation = grepTool.build(params);
            // Abort immediately before starting the search
            controller.abort();
            const result = await invocation.execute({
                abortSignal: controller.signal,
            });
            expect(result.returnDisplay.summary).toContain('No matches found');
        });
    });
    describe('error handling and edge cases', () => {
        it('should handle workspace boundary violations', () => {
            const params = {
                pattern: 'test',
                dir_path: '../outside',
            };
            expect(() => grepTool.build(params)).toThrow(/Path not in workspace/);
        });
        it.each([
            {
                name: 'empty directories',
                setup: async () => {
                    const emptyDir = path.join(tempRootDir, 'empty');
                    await fs.mkdir(emptyDir);
                    return { pattern: 'test', dir_path: 'empty' };
                },
            },
            {
                name: 'empty files',
                setup: async () => {
                    await fs.writeFile(path.join(tempRootDir, 'empty.txt'), '');
                    return { pattern: 'anything' };
                },
            },
        ])('should handle $name gracefully', async ({ setup }) => {
            mockSpawn.mockImplementation(createMockSpawn({ exitCode: 1 }));
            const params = await setup();
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('No matches found');
        });
        it('should handle special characters in file names', async () => {
            const specialFileName = 'file with spaces & symbols!.txt';
            await fs.writeFile(path.join(tempRootDir, specialFileName), 'hello world with special chars');
            // Setup specific mock for this test - searching for 'world' should find the file with special characters
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: specialFileName },
                        line_number: 1,
                        lines: { text: 'hello world with special chars\n' },
                    },
                }) + '\n',
            }));
            const params = { pattern: 'world' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain(specialFileName);
            expect(result.llmContent).toContain('hello world with special chars');
        });
        it('should handle deeply nested directories', async () => {
            const deepPath = path.join(tempRootDir, 'a', 'b', 'c', 'd', 'e');
            await fs.mkdir(deepPath, { recursive: true });
            await fs.writeFile(path.join(deepPath, 'deep.txt'), 'content in deep directory');
            // Setup specific mock for this test - searching for 'deep' should find the deeply nested file
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'a/b/c/d/e/deep.txt' },
                        line_number: 1,
                        lines: { text: 'content in deep directory\n' },
                    },
                }) + '\n',
            }));
            const params = { pattern: 'deep' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('deep.txt');
            expect(result.llmContent).toContain('content in deep directory');
        });
    });
    describe('regex pattern validation', () => {
        it('should handle complex regex patterns', async () => {
            await fs.writeFile(path.join(tempRootDir, 'code.js'), 'function getName() { return "test"; }\nconst getValue = () => "value";');
            // Setup specific mock for this test - regex pattern should match function declarations
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'code.js' },
                        line_number: 1,
                        lines: { text: 'function getName() { return "test"; }\n' },
                    },
                }) + '\n',
            }));
            const params = {
                pattern: 'function\\s+\\w+\\s*\\(',
                context: 0,
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('function getName()');
            expect(result.llmContent).not.toContain('const getValue');
        });
        it('should handle case sensitivity correctly in JS fallback', async () => {
            await fs.writeFile(path.join(tempRootDir, 'case.txt'), 'Hello World\nhello world\nHELLO WORLD');
            // Setup specific mock for this test - case insensitive search should match all variants
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'case.txt' },
                        line_number: 1,
                        lines: { text: 'Hello World\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'case.txt' },
                            line_number: 2,
                            lines: { text: 'hello world\n' },
                        },
                    }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'case.txt' },
                            line_number: 3,
                            lines: { text: 'HELLO WORLD\n' },
                        },
                    }) +
                    '\n',
            }));
            const params = { pattern: 'hello' };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Hello World');
            expect(result.llmContent).toContain('hello world');
            expect(result.llmContent).toContain('HELLO WORLD');
        });
        it('should handle escaped regex special characters', async () => {
            await fs.writeFile(path.join(tempRootDir, 'special.txt'), 'Price: $19.99\nRegex: [a-z]+ pattern\nEmail: test@example.com');
            // Setup specific mock for this test - escaped regex pattern should match price format
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'special.txt' },
                        line_number: 1,
                        lines: { text: 'Price: $19.99\n' },
                    },
                }) + '\n',
            }));
            const params = {
                pattern: '\\$\\d+\\.\\d+',
                context: 0,
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Price: $19.99');
            expect(result.llmContent).not.toContain('Email: test@example.com');
        });
    });
    describe('include pattern filtering', () => {
        it('should handle multiple file extensions in include pattern', async () => {
            await fs.writeFile(path.join(tempRootDir, 'test.ts'), 'typescript content');
            await fs.writeFile(path.join(tempRootDir, 'test.tsx'), 'tsx content');
            await fs.writeFile(path.join(tempRootDir, 'test.js'), 'javascript content');
            await fs.writeFile(path.join(tempRootDir, 'test.txt'), 'text content');
            // Setup specific mock for this test - include pattern should filter to only ts/tsx files
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'test.ts' },
                        line_number: 1,
                        lines: { text: 'typescript content\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'test.tsx' },
                            line_number: 1,
                            lines: { text: 'tsx content\n' },
                        },
                    }) +
                    '\n',
            }));
            const params = {
                pattern: 'content',
                include_pattern: '*.{ts,tsx}',
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('test.ts');
            expect(result.llmContent).toContain('test.tsx');
            expect(result.llmContent).not.toContain('test.js');
            expect(result.llmContent).not.toContain('test.txt');
        });
        it('should handle directory patterns in include', async () => {
            await fs.mkdir(path.join(tempRootDir, 'src'), { recursive: true });
            await fs.writeFile(path.join(tempRootDir, 'src', 'main.ts'), 'source code');
            await fs.writeFile(path.join(tempRootDir, 'other.ts'), 'other code');
            // Setup specific mock for this test - include pattern should filter to only src/** files
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'src/main.ts' },
                        line_number: 1,
                        lines: { text: 'source code\n' },
                    },
                }) + '\n',
            }));
            const params = {
                pattern: 'code',
                include_pattern: 'src/**',
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('main.ts');
            expect(result.llmContent).not.toContain('other.ts');
        });
    });
    describe('advanced search options', () => {
        it('should handle case_sensitive parameter', async () => {
            // Case-insensitive search (default)
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello world\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            let params = { pattern: 'HELLO', context: 0 };
            let invocation = grepTool.build(params);
            let result = await invocation.execute({ abortSignal });
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.arrayContaining(['--ignore-case']), expect.anything());
            expect(result.llmContent).toContain('Found 1 match for pattern "HELLO"');
            expect(result.llmContent).toContain('L1: hello world');
            // Case-sensitive search
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'HELLO world\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            params = { pattern: 'HELLO', case_sensitive: true, context: 0 };
            invocation = grepTool.build(params);
            result = await invocation.execute({ abortSignal });
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.not.arrayContaining(['--ignore-case']), expect.anything());
            expect(result.llmContent).toContain('Found 1 match for pattern "HELLO"');
            expect(result.llmContent).toContain('L1: HELLO world');
        });
        it('should handle fixed_strings parameter', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello.world\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const invocation = grepTool.build({
                pattern: 'hello.world',
                fixed_strings: true,
            });
            const result = await invocation.execute({ abortSignal });
            const spawnArgs = mockSpawn.mock.calls[0][1];
            expect(spawnArgs).toContain('--fixed-strings');
            expect(spawnArgs).toContain('--regexp');
            expect(spawnArgs).toContain('hello.world');
            // Verify --fixed-strings doesn't have the pattern as its next argument
            const fixedStringsIdx = spawnArgs.indexOf('--fixed-strings');
            expect(spawnArgs[fixedStringsIdx + 1]).not.toBe('hello.world');
            expect(result.llmContent).toContain('Found 1 match for pattern "hello.world"');
        });
        it('should allow invalid regex patterns when fixed_strings is true', () => {
            const params = {
                pattern: '[[',
                fixed_strings: true,
            };
            expect(grepTool.validateToolParams(params)).toBeNull();
        });
        it('should handle no_ignore parameter', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'ignored.log' },
                        line_number: 1,
                        lines: { text: 'secret log entry\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'secret', no_ignore: true };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.arrayContaining(['--no-ignore']), expect.anything());
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.not.arrayContaining(['--glob', '!node_modules']), expect.anything());
            expect(result.llmContent).toContain('Found 1 match for pattern "secret"');
            expect(result.llmContent).toContain('File: ignored.log');
            expect(result.llmContent).toContain('L1: secret log entry');
        });
        it('should disable gitignore rules when respectGitIgnore is false', async () => {
            const configWithoutGitIgnore = {
                getTargetDir: () => tempRootDir,
                getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
                getDebugMode: () => false,
                getFileFilteringRespectGitIgnore: () => false,
                getFileFilteringRespectGeminiIgnore: () => true,
                getFileFilteringOptions: () => ({
                    respectGitIgnore: false,
                    respectGeminiIgnore: true,
                }),
                storage: {
                    getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
                },
                isPathAllowed(absolutePath) {
                    const workspaceContext = this.getWorkspaceContext();
                    if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
                        return true;
                    }
                    const projectTempDir = this.storage.getProjectTempDir();
                    return isSubpath(path.resolve(projectTempDir), absolutePath);
                },
                validatePathAccess(absolutePath) {
                    if (this.isPathAllowed(absolutePath)) {
                        return null;
                    }
                    const workspaceDirs = this.getWorkspaceContext().getDirectories();
                    const projectTempDir = this.storage.getProjectTempDir();
                    return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
                },
            };
            const gitIgnoreDisabledTool = new RipGrepTool(configWithoutGitIgnore, createMockMessageBus());
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'ignored.log' },
                        line_number: 1,
                        lines: { text: 'secret log entry\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'secret' };
            const invocation = gitIgnoreDisabledTool.build(params);
            await invocation.execute({ abortSignal });
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.arrayContaining(['--no-ignore-vcs', '--no-ignore-exclude']), expect.anything());
        });
        it('should add .geminiignore when enabled and patterns exist', async () => {
            const geminiIgnorePath = path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME);
            await fs.writeFile(geminiIgnorePath, 'ignored.log');
            const configWithGeminiIgnore = {
                getTargetDir: () => tempRootDir,
                getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
                getDebugMode: () => false,
                getFileFilteringRespectGitIgnore: () => true,
                getFileFilteringRespectGeminiIgnore: () => true,
                getFileFilteringOptions: () => ({
                    respectGitIgnore: true,
                    respectGeminiIgnore: true,
                }),
                storage: {
                    getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
                },
                isPathAllowed(absolutePath) {
                    const workspaceContext = this.getWorkspaceContext();
                    if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
                        return true;
                    }
                    const projectTempDir = this.storage.getProjectTempDir();
                    return isSubpath(path.resolve(projectTempDir), absolutePath);
                },
                validatePathAccess(absolutePath) {
                    if (this.isPathAllowed(absolutePath)) {
                        return null;
                    }
                    const workspaceDirs = this.getWorkspaceContext().getDirectories();
                    const projectTempDir = this.storage.getProjectTempDir();
                    return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
                },
            };
            const geminiIgnoreTool = new RipGrepTool(configWithGeminiIgnore, createMockMessageBus());
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'ignored.log' },
                        line_number: 1,
                        lines: { text: 'secret log entry\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'secret' };
            const invocation = geminiIgnoreTool.build(params);
            await invocation.execute({ abortSignal });
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.arrayContaining(['--ignore-file', geminiIgnorePath]), expect.anything());
        });
        it('should skip .geminiignore when disabled', async () => {
            const geminiIgnorePath = path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME);
            await fs.writeFile(geminiIgnorePath, 'ignored.log');
            const configWithoutGeminiIgnore = {
                getTargetDir: () => tempRootDir,
                getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
                getDebugMode: () => false,
                getFileFilteringRespectGitIgnore: () => true,
                getFileFilteringRespectGeminiIgnore: () => false,
                getFileFilteringOptions: () => ({
                    respectGitIgnore: true,
                    respectGeminiIgnore: false,
                }),
                storage: {
                    getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
                },
                isPathAllowed(absolutePath) {
                    const workspaceContext = this.getWorkspaceContext();
                    if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
                        return true;
                    }
                    const projectTempDir = this.storage.getProjectTempDir();
                    return isSubpath(path.resolve(projectTempDir), absolutePath);
                },
                validatePathAccess(absolutePath) {
                    if (this.isPathAllowed(absolutePath)) {
                        return null;
                    }
                    const workspaceDirs = this.getWorkspaceContext().getDirectories();
                    const projectTempDir = this.storage.getProjectTempDir();
                    return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
                },
            };
            const geminiIgnoreTool = new RipGrepTool(configWithoutGeminiIgnore, createMockMessageBus());
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'ignored.log' },
                        line_number: 1,
                        lines: { text: 'secret log entry\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'secret' };
            const invocation = geminiIgnoreTool.build(params);
            await invocation.execute({ abortSignal });
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.not.arrayContaining(['--ignore-file', geminiIgnorePath]), expect.anything());
        });
        it('should handle context parameters', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'context',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello world\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileA.txt' },
                            line_number: 2,
                            lines: { text: 'second line with world\n' },
                        },
                    }) +
                    '\n' +
                    JSON.stringify({
                        type: 'context',
                        data: {
                            path: { text: 'fileA.txt' },
                            line_number: 3,
                            lines: { text: 'third line\n' },
                        },
                    }) +
                    '\n' +
                    JSON.stringify({
                        type: 'context',
                        data: {
                            path: { text: 'fileA.txt' },
                            line_number: 4,
                            lines: { text: 'fourth line\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = {
                pattern: 'world',
                context: 1,
                after: 2,
                before: 1,
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(mockSpawn).toHaveBeenLastCalledWith(expect.anything(), expect.arrayContaining([
                '--context',
                '1',
                '--after-context',
                '2',
                '--before-context',
                '1',
            ]), expect.anything());
            expect(result.llmContent).toContain('Found 1 match for pattern "world"');
            expect(result.llmContent).toContain('File: fileA.txt');
            expect(result.llmContent).toContain('L1- hello world');
            expect(result.llmContent).toContain('L2: second line with world');
            expect(result.llmContent).toContain('L3- third line');
            expect(result.llmContent).toContain('L4- fourth line');
        });
    });
    describe('getDescription', () => {
        it.each([
            {
                name: 'pattern only',
                params: { pattern: 'testPattern' },
                expected: "'testPattern' within ./",
            },
            {
                name: 'pattern and include',
                params: { pattern: 'testPattern', include_pattern: '*.ts' },
                expected: "'testPattern' in *.ts within ./",
            },
            {
                name: 'root path in description',
                params: { pattern: 'testPattern', dir_path: '.' },
                expected: "'testPattern' within ./",
            },
        ])('should generate correct description with $name', ({ params, expected }) => {
            const invocation = grepTool.build(params);
            expect(invocation.getDescription()).toBe(expected);
        });
        it('should generate correct description with pattern and path', async () => {
            const dirPath = path.join(tempRootDir, 'src', 'app');
            await fs.mkdir(dirPath, { recursive: true });
            const params = {
                pattern: 'testPattern',
                dir_path: path.join('src', 'app'),
            };
            const invocation = grepTool.build(params);
            expect(invocation.getDescription()).toContain("'testPattern' within");
            expect(invocation.getDescription()).toContain(path.join('src', 'app'));
        });
        it('should use ./ when no path is specified (defaults to CWD)', () => {
            const multiDirConfig = {
                getTargetDir: () => tempRootDir,
                getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir, ['/another/dir']),
                getDebugMode: () => false,
                getFileFilteringOptions: () => ({
                    respectGitIgnore: true,
                    respectGeminiIgnore: true,
                }),
                storage: {
                    getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
                },
                isPathAllowed(absolutePath) {
                    const workspaceContext = this.getWorkspaceContext();
                    if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
                        return true;
                    }
                    const projectTempDir = this.storage.getProjectTempDir();
                    return isSubpath(path.resolve(projectTempDir), absolutePath);
                },
                validatePathAccess(absolutePath) {
                    if (this.isPathAllowed(absolutePath)) {
                        return null;
                    }
                    const workspaceDirs = this.getWorkspaceContext().getDirectories();
                    const projectTempDir = this.storage.getProjectTempDir();
                    return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
                },
            };
            const multiDirGrepTool = new RipGrepTool(multiDirConfig, createMockMessageBus());
            const params = { pattern: 'testPattern' };
            const invocation = multiDirGrepTool.build(params);
            expect(invocation.getDescription()).toBe("'testPattern' within ./");
        });
        it('should generate correct description with pattern, include, and path', async () => {
            const dirPath = path.join(tempRootDir, 'src', 'app');
            await fs.mkdir(dirPath, { recursive: true });
            const params = {
                pattern: 'testPattern',
                include_pattern: '*.ts',
                dir_path: path.join('src', 'app'),
            };
            const invocation = grepTool.build(params);
            expect(invocation.getDescription()).toContain("'testPattern' in *.ts within");
            expect(invocation.getDescription()).toContain(path.join('src', 'app'));
        });
    });
    describe('new parameters', () => {
        it('should pass --max-count when max_matches_per_file is provided', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello world\n' },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = {
                pattern: 'world',
                max_matches_per_file: 1,
            };
            const invocation = grepTool.build(params);
            await invocation.execute({ abortSignal });
            const spawnArgs = mockSpawn.mock.calls[0][1];
            expect(spawnArgs).toContain('--max-count');
            expect(spawnArgs).toContain('1');
        });
        it('should respect total_max_matches and truncate results', async () => {
            // Return 3 matches, but set total_max_matches to 2
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'match 1\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileA.txt' },
                            line_number: 2,
                            lines: { text: 'match 2\n' },
                        },
                    }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileA.txt' },
                            line_number: 3,
                            lines: { text: 'match 3\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = {
                pattern: 'match',
                total_max_matches: 2,
                context: 0,
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 2 matches');
            expect(result.llmContent).toContain('results limited to 2 matches for performance');
            expect(result.llmContent).toContain('L1: match 1');
            expect(result.llmContent).toContain('L2: match 2');
            expect(result.llmContent).not.toContain('L3: match 3');
            expect(result.returnDisplay.summary).toBe('Found 2 matches (limited)');
        });
        it('should return only file paths when names_only is true', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'hello world\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileB.txt' },
                            line_number: 5,
                            lines: { text: 'hello again\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = {
                pattern: 'hello',
                names_only: true,
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 2 files with matches');
            expect(result.llmContent).toContain('fileA.txt');
            expect(result.llmContent).toContain('fileB.txt');
            expect(result.llmContent).not.toContain('L1:');
            expect(result.llmContent).not.toContain('hello world');
        });
        it('should filter out matches based on exclude_pattern', async () => {
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'fileA.txt' },
                        line_number: 1,
                        lines: { text: 'Copyright 2025 Google LLC\n' },
                    },
                }) +
                    '\n' +
                    JSON.stringify({
                        type: 'match',
                        data: {
                            path: { text: 'fileB.txt' },
                            line_number: 1,
                            lines: { text: 'Copyright 2026 Google LLC\n' },
                        },
                    }) +
                    '\n',
                exitCode: 0,
            }));
            const params = {
                pattern: 'Copyright .* Google LLC',
                exclude_pattern: '2026',
                context: 0,
            };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            expect(result.llmContent).toContain('Found 1 match');
            expect(result.llmContent).toContain('fileA.txt');
            expect(result.llmContent).not.toContain('fileB.txt');
            expect(result.llmContent).toContain('Copyright 2025 Google LLC');
        });
        it('should truncate excessively long lines', async () => {
            const longString = 'a'.repeat(3000);
            mockSpawn.mockImplementation(createMockSpawn({
                outputData: JSON.stringify({
                    type: 'match',
                    data: {
                        path: { text: 'longline.txt' },
                        line_number: 1,
                        lines: { text: `Target match ${longString}\n` },
                    },
                }) + '\n',
                exitCode: 0,
            }));
            const params = { pattern: 'Target match', context: 0 };
            const invocation = grepTool.build(params);
            const result = await invocation.execute({ abortSignal });
            // MAX_LINE_LENGTH_TEXT_FILE is 2000. It should be truncated.
            expect(result.llmContent).toContain('... [truncated]');
            expect(result.llmContent).not.toContain(longString);
        });
    });
});
describe('getRipgrepPath', () => {
    afterEach(() => {
        vi.restoreAllMocks();
    });
    describe('OS/Architecture Resolution', () => {
        it.each([
            { platform: 'darwin', arch: 'arm64', expectedBin: 'rg-darwin-arm64' },
            { platform: 'darwin', arch: 'x64', expectedBin: 'rg-darwin-x64' },
            { platform: 'linux', arch: 'arm64', expectedBin: 'rg-linux-arm64' },
            { platform: 'linux', arch: 'x64', expectedBin: 'rg-linux-x64' },
            { platform: 'win32', arch: 'x64', expectedBin: 'rg-win32-x64.exe' },
        ])('should map $platform $arch to $expectedBin', async ({ platform, arch, expectedBin }) => {
            vi.spyOn(os, 'platform').mockReturnValue(platform);
            vi.spyOn(os, 'arch').mockReturnValue(arch);
            vi.mocked(fileExists).mockImplementation(async (checkPath) => checkPath.endsWith(expectedBin));
            const resolvedPath = await getRipgrepPath();
            expect(resolvedPath).not.toBeNull();
            expect(resolvedPath?.endsWith(expectedBin)).toBe(true);
        });
    });
    describe('Path Fallback Logic', () => {
        beforeEach(() => {
            vi.spyOn(os, 'platform').mockReturnValue('linux');
            vi.spyOn(os, 'arch').mockReturnValue('x64');
        });
        it('should resolve the SEA (flattened) path first', async () => {
            vi.mocked(fileExists).mockImplementation(async (checkPath) => checkPath.includes(path.normalize('tools/vendor/ripgrep')));
            const resolvedPath = await getRipgrepPath();
            expect(resolvedPath).not.toBeNull();
            expect(resolvedPath).toContain(path.normalize('tools/vendor/ripgrep'));
        });
        it('should fall back to the Dev path if SEA path is missing', async () => {
            vi.mocked(fileExists).mockImplementation(async (checkPath) => checkPath.includes(path.normalize('core/vendor/ripgrep')) &&
                !checkPath.includes(path.join(path.sep, 'tools', path.sep)));
            const resolvedPath = await getRipgrepPath();
            expect(resolvedPath).not.toBeNull();
            expect(resolvedPath).toContain(path.normalize('core/vendor/ripgrep'));
            expect(resolvedPath).not.toContain(path.join(path.sep, 'tools', path.sep));
        });
        it('should return null if binary is missing from both paths', async () => {
            vi.mocked(fileExists).mockResolvedValue(false);
            const resolvedPath = await getRipgrepPath();
            expect(resolvedPath).toBeNull();
        });
    });
});
//# sourceMappingURL=ripGrep.test.js.map