CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
ripGrep.test.ts1535 linesDownload Raw Back to tools
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import {8  describe,9  it,10  expect,11  beforeEach,12  afterEach,13  vi,14  type Mock,15} from 'vitest';16import type { RipGrepToolParams } from './ripGrep.js';17import { _resetRipGrepCachesForTest, RipGrepTool } from './ripGrep.js';18import path from 'node:path';19import fsSync from 'node:fs';20import fs from 'node:fs/promises';21import os, { EOL } from 'node:os';22import type { Config } from '../config/config.js';23import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';24import { spawn } from 'node:child_process';25import { runRipgrep } from '../utils/ripgrepUtils.js';26import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';27import { FileReadCache } from '../services/fileReadCache.js';28 29// Mock ripgrepUtils30vi.mock('../utils/ripgrepUtils.js', () => ({31  runRipgrep: vi.fn(),32}));33 34// Mock child_process for ripgrep calls35vi.mock('child_process', () => ({36  spawn: vi.fn(),37}));38 39const mockSpawn = vi.mocked(spawn);40 41describe('RipGrepTool', () => {42  let tempRootDir: string;43  let grepTool: RipGrepTool;44  let fileExclusionsMock: { getGlobExcludes: () => string[] };45  let fileReadCache: FileReadCache;46  const abortSignal = new AbortController().signal;47  const sep = '\x1f';48 49  const mockConfig = {50    getTargetDir: () => tempRootDir,51    getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),52    getWorkingDir: () => tempRootDir,53    getDebugMode: () => false,54    getUseBuiltinRipgrep: () => true,55    getTruncateToolOutputThreshold: () => 25000,56    getTruncateToolOutputLines: () => 1000,57  } as unknown as Config;58 59  beforeEach(async () => {60    vi.clearAllMocks();61    mockSpawn.mockReset();62    _resetRipGrepCachesForTest();63    Object.assign(mockConfig, {64      getTruncateToolOutputThreshold: () => 25000,65    });66    tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));67    fileExclusionsMock = {68      getGlobExcludes: vi.fn().mockReturnValue([]),69    };70    fileReadCache = new FileReadCache();71    Object.assign(mockConfig, {72      getFileExclusions: () => fileExclusionsMock,73      getFileFilteringOptions: () => DEFAULT_FILE_FILTERING_OPTIONS,74      getFileReadCache: () => fileReadCache,75      getFileReadCacheDisabled: () => false,76    });77    grepTool = new RipGrepTool(mockConfig);78 79    // Create some test files and directories80    await fs.writeFile(81      path.join(tempRootDir, 'fileA.txt'),82      'hello world\nsecond line with world',83    );84    await fs.writeFile(85      path.join(tempRootDir, 'fileB.js'),86      'const foo = "bar";\nfunction baz() { return "hello"; }',87    );88    await fs.mkdir(path.join(tempRootDir, 'sub'));89    await fs.writeFile(90      path.join(tempRootDir, 'sub', 'fileC.txt'),91      'another world in sub dir',92    );93    await fs.writeFile(94      path.join(tempRootDir, 'sub', 'fileD.md'),95      '# Markdown file\nThis is a test.',96    );97  });98 99  afterEach(async () => {100    await fs.rm(tempRootDir, { recursive: true, force: true });101  });102 103  describe('validateToolParams', () => {104    it('should return null for valid params (pattern only)', () => {105      const params: RipGrepToolParams = { pattern: 'hello' };106      expect(grepTool.validateToolParams(params)).toBeNull();107    });108 109    it('should return null for valid params (pattern and path)', () => {110      const params: RipGrepToolParams = { pattern: 'hello', path: '.' };111      expect(grepTool.validateToolParams(params)).toBeNull();112    });113 114    it('should return null for valid params (pattern, path, and glob)', () => {115      const params: RipGrepToolParams = {116        pattern: 'hello',117        path: '.',118        glob: '*.txt',119      };120      expect(grepTool.validateToolParams(params)).toBeNull();121    });122 123    it('should return null for a positive integer limit', () => {124      const params: RipGrepToolParams = { pattern: 'hello', limit: 2 };125      expect(grepTool.validateToolParams(params)).toBeNull();126    });127 128    it.each([129      [0, 'params/limit must be >= 1'],130      [-1, 'params/limit must be >= 1'],131      [1.5, 'params/limit must be integer'],132    ])('should return error for invalid limit %s', (limit, expectedError) => {133      const params: RipGrepToolParams = { pattern: 'hello', limit };134      expect(grepTool.validateToolParams(params)).toBe(expectedError);135    });136 137    it('should return error if pattern is missing', () => {138      const params = { path: '.' } as unknown as RipGrepToolParams;139      expect(grepTool.validateToolParams(params)).toBe(140        `params must have required property 'pattern'`,141      );142    });143 144    it('should surface an error for invalid regex pattern', () => {145      const params: RipGrepToolParams = { pattern: '[[' };146      expect(grepTool.validateToolParams(params)).toContain(147        'Invalid regular expression pattern: [[',148      );149    });150 151    it('should return error if path does not exist', () => {152      const params: RipGrepToolParams = {153        pattern: 'hello',154        path: 'nonexistent',155      };156      // Check for the core error message, as the full path might vary157      expect(grepTool.validateToolParams(params)).toContain(158        'Path does not exist:',159      );160      expect(grepTool.validateToolParams(params)).toContain('nonexistent');161    });162 163    it('should allow path to be a file', () => {164      const filePath = path.join(tempRootDir, 'fileA.txt');165      const params: RipGrepToolParams = { pattern: 'hello', path: filePath };166      expect(grepTool.validateToolParams(params)).toBeNull();167    });168 169    it.skipIf(process.platform === 'win32')(170      'should unescape shell-escaped path',171      async () => {172        // Create a directory with a space so the unescaped path exists173        const dirWithSpace = path.join(tempRootDir, 'sub dir');174        await fs.mkdir(dirWithSpace);175        const params: RipGrepToolParams = {176          pattern: 'hello',177          path: path.join(tempRootDir, 'sub\\ dir'),178        };179        expect(grepTool.validateToolParams(params)).toBeNull();180        expect(params.path).toBe(dirWithSpace);181      },182    );183  });184 185  describe('execute', () => {186    it('should find matches for a simple pattern in all files', async () => {187      (runRipgrep as Mock).mockResolvedValue({188        stdout: `fileA.txt${sep}1${sep}hello world${EOL}fileA.txt${sep}2${sep}second line with world${EOL}sub/fileC.txt${sep}1${sep}another world in sub dir${EOL}`,189        truncated: false,190        error: undefined,191      });192 193      const params: RipGrepToolParams = { pattern: 'world' };194      const invocation = grepTool.build(params);195      const result = await invocation.execute(abortSignal);196      expect(result.llmContent).toContain(197        'Found 3 matches for pattern "world" in the workspace directory',198      );199      expect(result.llmContent).toContain('fileA.txt:1:hello world');200      expect(result.llmContent).toContain('fileA.txt:2:second line with world');201      expect(result.llmContent).toContain(202        'sub/fileC.txt:1:another world in sub dir',203      );204      expect(result.returnDisplay).toBe('Found 3 matches');205      expect(result.resultFilePaths).toEqual([206        path.join(tempRootDir, 'fileA.txt'),207        path.join(tempRootDir, 'sub/fileC.txt'),208      ]);209 210      const fileAStats = await fs.stat(path.join(tempRootDir, 'fileA.txt'));211      const fileCStats = await fs.stat(path.join(tempRootDir, 'sub/fileC.txt'));212      const fileARead = fileReadCache.check(fileAStats);213      const fileCRead = fileReadCache.check(fileCStats);214      expect(fileARead.state).toBe('fresh');215      expect(fileCRead.state).toBe('fresh');216      if (fileARead.state === 'fresh') {217        expect(fileARead.entry.lastReadWasFull).toBe(false);218        expect(fileARead.entry.lastReadCacheable).toBe(true);219      }220      if (fileCRead.state === 'fresh') {221        expect(fileCRead.entry.lastReadWasFull).toBe(false);222        expect(fileCRead.entry.lastReadCacheable).toBe(true);223      }224    });225 226    it('should treat summary-only JSON output as no matches', async () => {227      (runRipgrep as Mock).mockResolvedValue({228        stdout: `${JSON.stringify({ type: 'summary', data: { stats: { matches: 0 } } })}${EOL}`,229        truncated: false,230        error: undefined,231      });232 233      const invocation = grepTool.build({ pattern: 'missing' });234      const result = await invocation.execute(abortSignal);235 236      expect(result.llmContent).toBe(237        'No matches found for pattern "missing" in the workspace directory.',238      );239      expect(result.returnDisplay).toBe('No matches found');240    });241 242    it('parses JSON match events and records result paths', async () => {243      (runRipgrep as Mock).mockResolvedValue({244        stdout: `${JSON.stringify({ type: 'match', data: { path: { text: 'src/foo.ts' }, lines: { text: 'content\n' }, line_number: 5 } })}${EOL}`,245        truncated: false,246        error: undefined,247      });248 249      const invocation = grepTool.build({ pattern: 'content' });250      const result = await invocation.execute(abortSignal);251 252      expect(result.llmContent).toContain('src/foo.ts:5:content');253      expect(result.resultFilePaths).toEqual([254        path.join(tempRootDir, 'src/foo.ts'),255      ]);256    });257 258    it('parses JSON match events with byte-encoded paths', async () => {259      const bytePath = 'src/byte-path.ts';260      (runRipgrep as Mock).mockResolvedValue({261        stdout: `${JSON.stringify({ type: 'match', data: { path: { bytes: Buffer.from(bytePath, 'utf8').toString('base64') }, lines: { text: 'content\n' }, line_number: 3 } })}${EOL}`,262        truncated: false,263        error: undefined,264      });265 266      const invocation = grepTool.build({ pattern: 'content' });267      const result = await invocation.execute(abortSignal);268 269      expect(result.llmContent).toContain('src/byte-path.ts:3:content');270      expect(result.resultFilePaths).toEqual([271        path.join(tempRootDir, bytePath),272      ]);273    });274 275    it('handles JSON match events without a lines field', async () => {276      (runRipgrep as Mock).mockResolvedValue({277        stdout: `${JSON.stringify({ type: 'match', data: { path: { text: 'fileA.txt' }, line_number: 1 } })}${EOL}`,278        truncated: false,279        error: undefined,280      });281 282      const invocation = grepTool.build({ pattern: 'hello' });283      const result = await invocation.execute(abortSignal);284 285      expect(result.llmContent).toContain('fileA.txt:1:');286      expect(result.resultFilePaths).toEqual([287        path.join(tempRootDir, 'fileA.txt'),288      ]);289    });290 291    it('surfaces ripgrep system-level truncation in display metadata', async () => {292      (runRipgrep as Mock).mockResolvedValue({293        stdout: `fileA.txt${sep}1${sep}hello world${EOL}`,294        truncated: true,295        error: undefined,296      });297 298      const invocation = grepTool.build({ pattern: 'hello' });299      const result = await invocation.execute(abortSignal);300 301      expect(result.returnDisplay).toBe('Found 1 match (truncated)');302      expect(result.llmContent).toContain('[0 lines truncated] ...');303    });304 305    it('should preserve absolute result paths reported by ripgrep', async () => {306      const absoluteMatchPath = path.join(307        tempRootDir,308        'packages/core/src/skills/target.ts',309      );310      (runRipgrep as Mock).mockResolvedValue({311        stdout: `${absoluteMatchPath}${sep}1${sep}CORE_HELPER_TARGET_MARKER${EOL}`,312        truncated: false,313        error: undefined,314      });315 316      const params: RipGrepToolParams = {317        pattern: 'CORE_HELPER_TARGET_MARKER',318        glob: '**/*.ts',319      };320      const invocation = grepTool.build(params);321      const result = await invocation.execute(abortSignal);322 323      expect(result.resultFilePaths).toEqual([absoluteMatchPath]);324    });325 326    it('should parse Windows-style absolute result paths reported by ripgrep', async () => {327      const absoluteMatchPath =328        'C:\\repo\\packages\\core\\src\\skills\\target.ts';329      (runRipgrep as Mock).mockResolvedValue({330        stdout: `${absoluteMatchPath}${sep}12${sep}CORE_HELPER_TARGET_MARKER${EOL}`,331        truncated: false,332        error: undefined,333      });334 335      const invocation = grepTool.build({336        pattern: 'CORE_HELPER_TARGET_MARKER',337        glob: '**/*.ts',338      });339      const result = await invocation.execute(abortSignal);340 341      expect(result.resultFilePaths).toEqual([absoluteMatchPath]);342    });343 344    it('includes result paths for partially rendered long file paths', async () => {345      Object.assign(mockConfig, {346        getTruncateToolOutputThreshold: () => 30,347      });348      const longPath = 'packages/core/src/skills/very-long-named-file.ts';349      (runRipgrep as Mock).mockResolvedValue({350        stdout: `${longPath}${sep}1${sep}visible marker${EOL}`,351        truncated: false,352        error: undefined,353      });354 355      const invocation = grepTool.build({ pattern: 'marker', glob: '**/*.ts' });356      const result = await invocation.execute(abortSignal);357 358      expect(result.returnDisplay).toContain('truncated');359      expect(result.llmContent).toContain('packages/core/src/skills/very');360      expect(result.resultFilePaths).toEqual([361        path.join(tempRootDir, longPath),362      ]);363    });364 365    it('only reports result paths for lines reached before character truncation', async () => {366      Object.assign(mockConfig, {367        getTruncateToolOutputThreshold: () => 25,368      });369      const visiblePath = 'a.ts';370      const hiddenPath = 'hidden-file-with-long-name.ts';371      (runRipgrep as Mock).mockResolvedValue({372        stdout: `${visiblePath}${sep}1${sep}visible marker${EOL}${hiddenPath}${sep}1${sep}hidden marker${EOL}`,373        truncated: false,374        error: undefined,375      });376 377      const invocation = grepTool.build({ pattern: 'marker', glob: '**/*.ts' });378      const result = await invocation.execute(abortSignal);379 380      expect(result.returnDisplay).toContain('truncated');381      expect(result.resultFilePaths).toEqual([382        path.join(tempRootDir, visiblePath),383        path.join(tempRootDir, hiddenPath),384      ]);385    });386 387    it('should find matches in a specific path', async () => {388      // Setup specific mock for this test - searching in 'sub' should only return matches from that directory389      (runRipgrep as Mock).mockResolvedValue({390        stdout: `fileC.txt:1:another world in sub dir${EOL}`,391        truncated: false,392        error: undefined,393      });394 395      const params: RipGrepToolParams = { pattern: 'world', path: 'sub' };396      const invocation = grepTool.build(params);397      const result = await invocation.execute(abortSignal);398      expect(result.llmContent).toContain(399        'Found 1 match for pattern "world" in path "sub"',400      );401      expect(result.llmContent).toContain(402        'fileC.txt:1:another world in sub dir',403      );404      expect(result.returnDisplay).toBe('Found 1 match');405    });406 407    it('should use target directory when path is not provided', async () => {408      (runRipgrep as Mock).mockResolvedValue({409        stdout: `fileA.txt:1:hello world${EOL}`,410        truncated: false,411        error: undefined,412      });413 414      const params: RipGrepToolParams = { pattern: 'world' };415      const invocation = grepTool.build(params);416      const result = await invocation.execute(abortSignal);417      expect(result.llmContent).toContain(418        'Found 1 match for pattern "world" in the workspace directory',419      );420    });421 422    it('should find matches with a glob filter', async () => {423      // Setup specific mock for this test424      (runRipgrep as Mock).mockResolvedValue({425        stdout: `fileB.js:2:function baz() { return "hello"; }${EOL}`,426        truncated: false,427        error: undefined,428      });429 430      const params: RipGrepToolParams = { pattern: 'hello', glob: '*.js' };431      const invocation = grepTool.build(params);432      const result = await invocation.execute(abortSignal);433      expect(result.llmContent).toContain(434        'Found 1 match for pattern "hello" in the workspace directory (filter: "*.js"):',435      );436      expect(result.llmContent).toContain(437        'fileB.js:2:function baz() { return "hello"; }',438      );439      expect(result.returnDisplay).toBe('Found 1 match');440    });441 442    it('should find matches with a glob filter and path', async () => {443      await fs.writeFile(444        path.join(tempRootDir, 'sub', 'another.js'),445        'const greeting = "hello";',446      );447 448      // Setup specific mock for this test - searching for 'hello' in 'sub' with '*.js' filter449      (runRipgrep as Mock).mockResolvedValue({450        stdout: `another.js:1:const greeting = "hello";${EOL}`,451        truncated: false,452        error: undefined,453      });454 455      const params: RipGrepToolParams = {456        pattern: 'hello',457        path: 'sub',458        glob: '*.js',459      };460      const invocation = grepTool.build(params);461      const result = await invocation.execute(abortSignal);462      expect(result.llmContent).toContain(463        'Found 1 match for pattern "hello" in path "sub" (filter: "*.js")',464      );465      expect(result.llmContent).toContain(466        'another.js:1:const greeting = "hello";',467      );468      expect(result.returnDisplay).toBe('Found 1 match');469    });470 471    it('should pass .qwenignore to ripgrep when respected', async () => {472      await fs.writeFile(473        path.join(tempRootDir, '.qwenignore'),474        'ignored.txt\n',475      );476      (runRipgrep as Mock).mockResolvedValue({477        stdout: '',478        truncated: false,479        error: undefined,480      });481 482      const params: RipGrepToolParams = { pattern: 'secret' };483      const invocation = grepTool.build(params);484      const result = await invocation.execute(abortSignal);485      expect(result.llmContent).toContain(486        'No matches found for pattern "secret" in the workspace directory.',487      );488      expect(result.returnDisplay).toBe('No matches found');489    });490 491    it('should include .qwenignore matches when disabled in config', async () => {492      await fs.writeFile(path.join(tempRootDir, '.qwenignore'), 'kept.txt\n');493      await fs.writeFile(path.join(tempRootDir, 'kept.txt'), 'keep me');494      Object.assign(mockConfig, {495        getFileFilteringOptions: () => ({496          respectGitIgnore: true,497          respectQwenIgnore: false,498        }),499      });500 501      (runRipgrep as Mock).mockResolvedValue({502        stdout: `kept.txt:1:keep me${EOL}`,503        truncated: false,504        error: undefined,505      });506 507      const params: RipGrepToolParams = { pattern: 'keep' };508      const invocation = grepTool.build(params);509      const result = await invocation.execute(abortSignal);510      expect(result.llmContent).toContain(511        'Found 1 match for pattern "keep" in the workspace directory:',512      );513      expect(result.llmContent).toContain('kept.txt:1:keep me');514      expect(result.returnDisplay).toBe('Found 1 match');515    });516 517    it('should disable gitignore when configured', async () => {518      Object.assign(mockConfig, {519        getFileFilteringOptions: () => ({520          respectGitIgnore: false,521          respectQwenIgnore: true,522        }),523      });524 525      (runRipgrep as Mock).mockResolvedValue({526        stdout: '',527        truncated: false,528        error: undefined,529      });530 531      const params: RipGrepToolParams = { pattern: 'ignored' };532      const invocation = grepTool.build(params);533      await invocation.execute(abortSignal);534    });535 536    it('should truncate llm content when exceeding maximum length', async () => {537      const longMatch = 'fileA.txt:1:' + 'a'.repeat(30_000);538 539      (runRipgrep as Mock).mockResolvedValue({540        stdout: `${longMatch}${EOL}`,541        truncated: false,542        error: undefined,543      });544 545      const params: RipGrepToolParams = { pattern: 'a+' };546      const invocation = grepTool.build(params);547      const result = await invocation.execute(abortSignal);548 549      expect(String(result.llmContent).length).toBeLessThanOrEqual(26_000);550      expect(result.llmContent).toMatch(/\[\d+ lines? truncated\] \.\.\./);551      expect(result.returnDisplay).toContain('truncated');552    });553 554    it('should return "No matches found" when pattern does not exist', async () => {555      // Setup specific mock for no matches556      (runRipgrep as Mock).mockResolvedValue({557        stdout: '',558        truncated: false,559        error: undefined,560      });561 562      const params: RipGrepToolParams = { pattern: 'nonexistentpattern' };563      const invocation = grepTool.build(params);564      const result = await invocation.execute(abortSignal);565      expect(result.llmContent).toContain(566        'No matches found for pattern "nonexistentpattern" in the workspace directory.',567      );568      expect(result.returnDisplay).toBe('No matches found');569    });570 571    it('should throw validation error for invalid regex pattern', async () => {572      const params: RipGrepToolParams = { pattern: '[[' };573      expect(() => grepTool.build(params)).toThrow(574        'Invalid regular expression pattern: [[',575      );576    });577 578    it('should handle regex special characters correctly', async () => {579      // Setup specific mock for this test - regex pattern 'foo.*bar' should match 'const foo = "bar";'580      (runRipgrep as Mock).mockResolvedValue({581        stdout: `fileB.js:1:const foo = "bar";${EOL}`,582        truncated: false,583        error: undefined,584      });585 586      const params: RipGrepToolParams = { pattern: 'foo.*bar' }; // Matches 'const foo = "bar";'587      const invocation = grepTool.build(params);588      const result = await invocation.execute(abortSignal);589      expect(result.llmContent).toContain(590        'Found 1 match for pattern "foo.*bar" in the workspace directory:',591      );592      expect(result.llmContent).toContain('fileB.js:1:const foo = "bar";');593    });594 595    it('should be case-insensitive by default (JS fallback)', async () => {596      // Setup specific mock for this test - case insensitive search for 'HELLO'597      (runRipgrep as Mock).mockResolvedValue({598        stdout: `fileA.txt:1:hello world${EOL}fileB.js:2:function baz() { return "hello"; }${EOL}`,599        truncated: false,600        error: undefined,601      });602 603      const params: RipGrepToolParams = { pattern: 'HELLO' };604      const invocation = grepTool.build(params);605      const result = await invocation.execute(abortSignal);606      expect(result.llmContent).toContain(607        'Found 2 matches for pattern "HELLO" in the workspace directory:',608      );609      expect(result.llmContent).toContain('fileA.txt:1:hello world');610      expect(result.llmContent).toContain(611        'fileB.js:2:function baz() { return "hello"; }',612      );613    });614 615    it('should throw an error if params are invalid', async () => {616      const params = { path: '.' } as unknown as RipGrepToolParams; // Invalid: pattern missing617      expect(() => grepTool.build(params)).toThrow(618        /params must have required property 'pattern'/,619      );620    });621 622    it('should search within a single file when path is a file', async () => {623      (runRipgrep as Mock).mockResolvedValue({624        stdout: `fileA.txt:1:hello world${EOL}fileA.txt:2:second line with world${EOL}`,625        truncated: false,626        error: undefined,627      });628 629      const params: RipGrepToolParams = {630        pattern: 'world',631        path: path.join(tempRootDir, 'fileA.txt'),632      };633      const invocation = grepTool.build(params);634      const result = await invocation.execute(abortSignal);635      expect(result.llmContent).toContain('Found 2 matches');636      expect(result.llmContent).toContain('fileA.txt:1:hello world');637      expect(result.llmContent).toContain('fileA.txt:2:second line with world');638      expect(result.returnDisplay).toBe('Found 2 matches');639    });640 641    it('should throw an error if ripgrep is not available', async () => {642      (runRipgrep as Mock).mockResolvedValue({643        stdout: '',644        truncated: false,645        error: new Error('ripgrep binary not found.'),646      });647 648      const params: RipGrepToolParams = { pattern: 'world' };649      const invocation = grepTool.build(params);650 651      expect(await invocation.execute(abortSignal)).toStrictEqual({652        llmContent:653          'Error during grep search operation: ripgrep binary not found.',654        returnDisplay: 'Error: ripgrep binary not found.',655      });656    });657 658    it('should pass useBuiltinRipgrep setting to ripgrep execution', async () => {659      const systemOnlyConfig = {660        ...mockConfig,661        getUseBuiltinRipgrep: () => false,662      } as unknown as Config;663      const systemOnlyGrepTool = new RipGrepTool(systemOnlyConfig);664 665      (runRipgrep as Mock).mockResolvedValue({666        stdout: `fileA.txt${sep}1${sep}hello world${EOL}`,667        truncated: false,668        error: undefined,669      });670 671      const params: RipGrepToolParams = { pattern: 'hello' };672      const invocation = systemOnlyGrepTool.build(params);673      await invocation.execute(abortSignal);674 675      expect(runRipgrep).toHaveBeenCalledWith(676        expect.any(Array),677        abortSignal,678        false,679      );680    });681  });682 683  describe('multi-directory workspace', () => {684    it('should search across all workspace directories when no path is specified', async () => {685      const secondDir = await fs.mkdtemp(686        path.join(os.tmpdir(), 'grep-tool-second-'),687      );688      await fs.writeFile(689        path.join(secondDir, 'extra.txt'),690        'hello from second dir',691      );692 693      const multiDirConfig = {694        ...mockConfig,695        getWorkspaceContext: () =>696          createMockWorkspaceContext(tempRootDir, [secondDir]),697      } as unknown as Config;698 699      const multiDirGrepTool = new RipGrepTool(multiDirConfig);700 701      (runRipgrep as Mock).mockResolvedValue({702        stdout: `fileA.txt${sep}1${sep}hello world${EOL}${secondDir}${path.sep}extra.txt${sep}1${sep}hello from second dir${EOL}`,703        truncated: false,704        error: undefined,705      });706 707      const params: RipGrepToolParams = { pattern: 'hello' };708      const invocation = multiDirGrepTool.build(params);709      const result = await invocation.execute(abortSignal);710 711      expect(result.llmContent).toContain('across 2 workspace directories');712      expect(result.llmContent).toContain('Found 2 matches');713      expect(result.resultFilePaths).toEqual([714        path.join(tempRootDir, 'fileA.txt'),715        path.join(secondDir, 'extra.txt'),716      ]);717 718      // Verify both paths were passed to runRipgrep719      expect(runRipgrep).toHaveBeenCalledWith(720        expect.arrayContaining([721          '--json',722          '--no-messages',723          tempRootDir,724          secondDir,725        ]),726        expect.anything(),727        true,728      );729 730      await fs.rm(secondDir, { recursive: true, force: true });731    });732 733    it('should search only specified path when path is given (ignoring multi-dir)', async () => {734      const secondDir = await fs.mkdtemp(735        path.join(os.tmpdir(), 'grep-tool-second-'),736      );737      await fs.writeFile(path.join(secondDir, 'other.txt'), 'other content');738 739      const multiDirConfig = {740        ...mockConfig,741        getWorkspaceContext: () =>742          createMockWorkspaceContext(tempRootDir, [secondDir]),743      } as unknown as Config;744 745      const multiDirGrepTool = new RipGrepTool(multiDirConfig);746 747      (runRipgrep as Mock).mockResolvedValue({748        stdout: `fileC.txt:1:another world in sub dir${EOL}`,749        truncated: false,750        error: undefined,751      });752 753      const params: RipGrepToolParams = { pattern: 'world', path: 'sub' };754      const invocation = multiDirGrepTool.build(params);755      const result = await invocation.execute(abortSignal);756 757      expect(result.llmContent).toContain('in path "sub"');758      expect(result.llmContent).not.toContain('across');759 760      await fs.rm(secondDir, { recursive: true, force: true });761    });762 763    it('should load .qwenignore from each workspace directory', async () => {764      const secondDir = await fs.mkdtemp(765        path.join(os.tmpdir(), 'grep-tool-second-'),766      );767      await fs.writeFile(path.join(secondDir, '.qwenignore'), 'ignored.txt\n');768      await fs.writeFile(769        path.join(tempRootDir, '.qwenignore'),770        'other-ignored.txt\n',771      );772 773      const multiDirConfig = {774        ...mockConfig,775        getWorkspaceContext: () =>776          createMockWorkspaceContext(tempRootDir, [secondDir]),777      } as unknown as Config;778 779      const multiDirGrepTool = new RipGrepTool(multiDirConfig);780 781      (runRipgrep as Mock).mockResolvedValue({782        stdout: '',783        truncated: false,784        error: undefined,785      });786 787      const params: RipGrepToolParams = { pattern: 'test' };788      const invocation = multiDirGrepTool.build(params);789      await invocation.execute(abortSignal);790 791      // Verify both .qwenignore files were passed792      const rgArgs = (runRipgrep as Mock).mock.calls[0][0] as string[];793      const ignoreFileArgs = rgArgs.filter(794        (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',795      );796      expect(ignoreFileArgs).toContain(path.join(tempRootDir, '.qwenignore'));797      expect(ignoreFileArgs).toContain(path.join(secondDir, '.qwenignore'));798 799      await fs.rm(secondDir, { recursive: true, force: true });800    });801 802    it('should pass .agentignore and .aiignore to ripgrep when respected', async () => {803      await fs.writeFile(804        path.join(tempRootDir, '.agentignore'),805        'agent-secret.txt\n',806      );807      await fs.writeFile(808        path.join(tempRootDir, '.aiignore'),809        'ai-secret.txt\n',810      );811 812      (runRipgrep as Mock).mockResolvedValue({813        stdout: '',814        truncated: false,815        error: undefined,816      });817 818      const params: RipGrepToolParams = { pattern: 'secret' };819      const invocation = grepTool.build(params);820      await invocation.execute(abortSignal);821 822      const rgArgs = (runRipgrep as Mock).mock.calls[0][0] as string[];823      const ignoreFileArgs = rgArgs.filter(824        (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',825      );826      expect(ignoreFileArgs).toContain(path.join(tempRootDir, '.agentignore'));827      expect(ignoreFileArgs).toContain(path.join(tempRootDir, '.aiignore'));828    });829 830    it('should pass non-qwen ignore files unchanged so ripgrep preserves negations', async () => {831      const qwenIgnorePath = path.join(tempRootDir, '.qwenignore');832      const agentIgnorePath = path.join(tempRootDir, '.agentignore');833 834      await fs.writeFile(qwenIgnorePath, '*.env\n');835      await fs.writeFile(836        agentIgnorePath,837        '*.env\n!allowed.env\n\\!literal.txt\n',838      );839 840      (runRipgrep as Mock).mockImplementation(async (rgArgs: string[]) => {841        const ignoreFileArgs = rgArgs.filter(842          (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',843        );844        expect(ignoreFileArgs).toContain(qwenIgnorePath);845        expect(ignoreFileArgs).toContain(agentIgnorePath);846        expect(ignoreFileArgs.indexOf(agentIgnorePath)).toBeLessThan(847          ignoreFileArgs.indexOf(qwenIgnorePath),848        );849 850        const agentIgnoreContent = await fs.readFile(agentIgnorePath, 'utf8');851        expect(agentIgnoreContent).toContain('!allowed.env');852 853        return {854          stdout: '',855          truncated: false,856          error: undefined,857        };858      });859 860      const invocation = grepTool.build({ pattern: 'API_KEY' });861      await invocation.execute(abortSignal);862    });863 864    it('should preserve negation semantics within the same non-qwen ignore file', async () => {865      await fs.writeFile(866        path.join(tempRootDir, '.agentignore'),867        '*.env\n!allowed.env\n',868      );869      await fs.writeFile(path.join(tempRootDir, 'blocked.env'), 'API_KEY=1');870      await fs.writeFile(path.join(tempRootDir, 'allowed.env'), 'API_KEY=2');871 872      (runRipgrep as Mock).mockResolvedValue({873        stdout: `blocked.env${sep}1${sep}API_KEY=1${EOL}allowed.env${sep}1${sep}API_KEY=2${EOL}`,874        truncated: false,875        error: undefined,876      });877 878      const invocation = grepTool.build({ pattern: 'API_KEY' });879      const result = await invocation.execute(abortSignal);880 881      expect(result.llmContent).toContain('Found 1 match');882      expect(result.llmContent).toContain('allowed.env:1:API_KEY=2');883      expect(result.llmContent).not.toContain('blocked.env');884      expect(result.returnDisplay).toBe('Found 1 match');885      expect(result.resultFilePaths).toEqual([886        path.join(tempRootDir, 'allowed.env'),887      ]);888    });889 890    it('should not let a custom ignore negation expose .qwenignore matches in grep output', async () => {891      const qwenIgnorePath = path.join(tempRootDir, '.qwenignore');892      const agentIgnorePath = path.join(tempRootDir, '.agentignore');893      await fs.writeFile(qwenIgnorePath, '*.env\n');894      await fs.writeFile(agentIgnorePath, '!*.env\n');895      await fs.writeFile(path.join(tempRootDir, 'allowed.env'), 'API_KEY=2');896 897      (runRipgrep as Mock).mockResolvedValue({898        stdout: `allowed.env${sep}1${sep}API_KEY=2${EOL}`,899        truncated: false,900        error: undefined,901      });902 903      const invocation = grepTool.build({ pattern: 'API_KEY' });904      const result = await invocation.execute(abortSignal);905 906      expect(result.llmContent).toContain('No matches found');907      expect(result.returnDisplay).toBe('No matches found');908 909      const rgArgs = (runRipgrep as Mock).mock.calls[0][0] as string[];910      const ignoreFileArgs = rgArgs.filter(911        (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',912      );913      expect(ignoreFileArgs).toEqual([agentIgnorePath, qwenIgnorePath]);914    });915 916    it('should post-filter matches ignored by another workspace .qwenignore', async () => {917      const secondDir = await fs.mkdtemp(918        path.join(os.tmpdir(), 'grep-tool-second-'),919      );920      await fs.writeFile(path.join(tempRootDir, '.qwenignore'), '*.env\n');921      await fs.writeFile(path.join(secondDir, '.qwenignore'), '!*.env\n');922      await fs.writeFile(path.join(tempRootDir, 'secret.env'), 'API_KEY=1');923      await fs.writeFile(path.join(tempRootDir, 'visible.txt'), 'API_KEY=2');924 925      const multiDirConfig = {926        ...mockConfig,927        getWorkspaceContext: () =>928          createMockWorkspaceContext(tempRootDir, [secondDir]),929      } as unknown as Config;930      const multiDirGrepTool = new RipGrepTool(multiDirConfig);931 932      (runRipgrep as Mock).mockResolvedValue({933        stdout: `secret.env${sep}1${sep}API_KEY=1${EOL}visible.txt${sep}1${sep}API_KEY=2${EOL}`,934        truncated: false,935        error: undefined,936      });937 938      const invocation = multiDirGrepTool.build({ pattern: 'API_KEY' });939      const result = await invocation.execute(abortSignal);940 941      expect(result.llmContent).toContain('Found 1 match');942      expect(result.llmContent).toContain('visible.txt:1:API_KEY=2');943      expect(result.llmContent).not.toContain('secret.env');944      expect(result.returnDisplay).toBe('Found 1 match');945      expect(result.resultFilePaths).toEqual([946        path.join(tempRootDir, 'visible.txt'),947      ]);948 949      await fs.rm(secondDir, { recursive: true, force: true });950    });951 952    it('should preserve negation semantics within the same .qwenignore', async () => {953      await fs.writeFile(954        path.join(tempRootDir, '.qwenignore'),955        '*.env\n!allowed.env\n',956      );957      await fs.writeFile(path.join(tempRootDir, 'blocked.env'), 'API_KEY=1');958      await fs.writeFile(path.join(tempRootDir, 'allowed.env'), 'API_KEY=2');959 960      (runRipgrep as Mock).mockResolvedValue({961        stdout: `blocked.env${sep}1${sep}API_KEY=1${EOL}allowed.env${sep}1${sep}API_KEY=2${EOL}`,962        truncated: false,963        error: undefined,964      });965 966      const invocation = grepTool.build({ pattern: 'API_KEY' });967      const result = await invocation.execute(abortSignal);968 969      expect(result.llmContent).toContain('Found 1 match');970      expect(result.llmContent).toContain('allowed.env:1:API_KEY=2');971      expect(result.llmContent).not.toContain('blocked.env');972      expect(result.returnDisplay).toBe('Found 1 match');973      expect(result.resultFilePaths).toEqual([974        path.join(tempRootDir, 'allowed.env'),975      ]);976    });977 978    it('should post-filter matches unignored by a custom nested .qwenignore', async () => {979      await fs.mkdir(path.join(tempRootDir, 'nested'));980      await fs.writeFile(path.join(tempRootDir, '.qwenignore'), '*.env\n');981      await fs.writeFile(982        path.join(tempRootDir, 'nested', '.qwenignore'),983        '!*.env\n',984      );985      await fs.writeFile(path.join(tempRootDir, 'secret.env'), 'API_KEY=1');986      await fs.writeFile(path.join(tempRootDir, 'visible.txt'), 'API_KEY=2');987      Object.assign(mockConfig, {988        getFileFilteringOptions: () => ({989          respectGitIgnore: true,990          respectQwenIgnore: true,991          customIgnoreFiles: ['nested/.qwenignore'],992        }),993      });994 995      (runRipgrep as Mock).mockResolvedValue({996        stdout: `secret.env${sep}1${sep}API_KEY=1${EOL}visible.txt${sep}1${sep}API_KEY=2${EOL}`,997        truncated: false,998        error: undefined,999      });1000 1001      const invocation = grepTool.build({ pattern: 'API_KEY' });1002      const result = await invocation.execute(abortSignal);1003 1004      expect(result.llmContent).toContain('Found 1 match');1005      expect(result.llmContent).toContain('visible.txt:1:API_KEY=2');1006      expect(result.llmContent).not.toContain('secret.env');1007      expect(result.returnDisplay).toBe('Found 1 match');1008      expect(result.resultFilePaths).toEqual([1009        path.join(tempRootDir, 'visible.txt'),1010      ]);1011 1012      const rgArgs = (runRipgrep as Mock).mock.calls[0][0] as string[];1013      const ignoreFileArgs = rgArgs.filter(1014        (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',1015      );1016      expect(ignoreFileArgs).toEqual([1017        path.join(tempRootDir, 'nested', '.qwenignore'),1018        path.join(tempRootDir, '.qwenignore'),1019      ]);1020    });1021 1022    it('should pass configured custom ignore files to ripgrep', async () => {1023      await fs.writeFile(1024        path.join(tempRootDir, '.cursorignore'),1025        'cursor-secret.txt\n',1026      );1027      await fs.writeFile(1028        path.join(tempRootDir, '.agentignore'),1029        'agent-secret.txt\n',1030      );1031      Object.assign(mockConfig, {1032        getFileFilteringOptions: () => ({1033          respectGitIgnore: true,1034          respectQwenIgnore: true,1035          customIgnoreFiles: ['.cursorignore'],1036        }),1037      });1038 1039      (runRipgrep as Mock).mockResolvedValue({1040        stdout: '',1041        truncated: false,1042        error: undefined,1043      });1044 1045      const params: RipGrepToolParams = { pattern: 'secret' };1046      const invocation = grepTool.build(params);1047      await invocation.execute(abortSignal);1048 1049      const rgArgs = (runRipgrep as Mock).mock.calls[0][0] as string[];1050      const ignoreFileArgs = rgArgs.filter(1051        (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',1052      );1053      expect(ignoreFileArgs).toContain(path.join(tempRootDir, '.cursorignore'));1054      expect(ignoreFileArgs).not.toContain(1055        path.join(tempRootDir, '.agentignore'),1056      );1057    });1058 1059    it('should resolve ignore files from the workspace root for subdirectory searches', async () => {1060      await fs.writeFile(1061        path.join(tempRootDir, '.cursorignore'),1062        'cursor-secret.txt\n',1063      );1064      await fs.writeFile(1065        path.join(tempRootDir, 'sub', '.cursorignore'),1066        'sub-secret.txt\n',1067      );1068      Object.assign(mockConfig, {1069        getFileFilteringOptions: () => ({1070          respectGitIgnore: true,1071          respectQwenIgnore: true,1072          customIgnoreFiles: ['.cursorignore'],1073        }),1074      });1075 1076      (runRipgrep as Mock).mockResolvedValue({1077        stdout: '',1078        truncated: false,1079        error: undefined,1080      });1081 1082      const params: RipGrepToolParams = {1083        pattern: 'secret',1084        path: 'sub',1085      };1086      const invocation = grepTool.build(params);1087      await invocation.execute(abortSignal);1088 1089      const rgArgs = (runRipgrep as Mock).mock.calls[0][0] as string[];1090      const ignoreFileArgs = rgArgs.filter(1091        (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',1092      );1093      expect(ignoreFileArgs).toContain(path.join(tempRootDir, '.cursorignore'));1094      expect(ignoreFileArgs).not.toContain(1095        path.join(tempRootDir, 'sub', '.cursorignore'),1096      );1097    });1098 1099    it('should not load ignore files from relative external search paths', async () => {1100      const testCwd = await fs.mkdtemp(1101        path.join(os.tmpdir(), 'grep-tool-cwd-'),1102      );1103      const outsideDir = path.join(testCwd, 'outside');1104      const originalCwd = process.cwd();1105 1106      try {1107        await fs.mkdir(outsideDir);1108        await fs.writeFile(1109          path.join(outsideDir, '.cursorignore'),1110          'cursor-secret.txt\n',1111        );1112        Object.assign(mockConfig, {1113          getFileFilteringOptions: () => ({1114            respectGitIgnore: true,1115            respectQwenIgnore: true,1116            customIgnoreFiles: ['.cursorignore'],1117          }),1118        });1119 1120        (runRipgrep as Mock).mockResolvedValue({1121          stdout: '',1122          truncated: false,1123          error: undefined,1124        });1125 1126        process.chdir(testCwd);1127 1128        const invocation = grepTool.build({1129          pattern: 'secret',1130        }) as unknown as {1131          performRipgrepSearch(options: {1132            pattern: string;1133            paths: string[];1134            signal: AbortSignal;1135          }): Promise<{ stdout: string; truncated: boolean }>;1136        };1137        await invocation.performRipgrepSearch({1138          pattern: 'secret',1139          paths: ['outside'],1140          signal: abortSignal,1141        });1142 1143        const rgArgs = (runRipgrep as Mock).mock.calls[0][0] as string[];1144        const ignoreFileArgs = rgArgs.filter(1145          (a: string, i: number) => i > 0 && rgArgs[i - 1] === '--ignore-file',1146        );1147        expect(ignoreFileArgs).toEqual([]);1148      } finally {1149        process.chdir(originalCwd);1150        await fs.rm(testCwd, { recursive: true, force: true });1151      }1152    });1153 1154    it('should cache resolved relative result paths across filtering and result metadata', async () => {1155      const existsSyncSpy = vi.spyOn(fsSync, 'existsSync');1156      const repeatedLine = `fileA.txt${sep}1${sep}hello world`;1157 1158      (runRipgrep as Mock).mockResolvedValue({1159        stdout: `${repeatedLine}${EOL}${repeatedLine}${EOL}${repeatedLine}${EOL}`,1160        truncated: false,1161        error: undefined,1162      });1163 1164      const invocation = grepTool.build({ pattern: 'hello' });1165      await invocation.execute(abortSignal);1166 1167      const fileAPath = path.join(tempRootDir, 'fileA.txt');1168      const fileAProbeCount = existsSyncSpy.mock.calls.filter(1169        ([candidate]) => String(candidate) === fileAPath,1170      ).length;1171      expect(fileAProbeCount).toBe(1);1172    });1173 1174    it('should deduplicate matches from overlapping workspace directories', async () => {1175      // This tests the fix: when ripgrep receives overlapping search paths1176      // (e.g. /parent and /parent/sub), it may report the same file twice.1177      // The deduplication layer must remove duplicates.1178      const subDir = path.join(tempRootDir, 'sub');1179 1180      const multiDirConfig = {1181        ...mockConfig,1182        getWorkspaceContext: () =>1183          createMockWorkspaceContext(tempRootDir, [subDir]),1184      } as unknown as Config;1185 1186      const multiDirGrepTool = new RipGrepTool(multiDirConfig);1187 1188      // Simulate ripgrep returning the same file:line twice (once from each search root)1189      const dupLine = `${path.join(subDir, 'fileC.txt')}${sep}1${sep}hello world`;1190      (runRipgrep as Mock).mockResolvedValue({1191        stdout: `${dupLine}${EOL}${dupLine}${EOL}`,1192        truncated: false,1193        error: undefined,1194      });1195 1196      const params: RipGrepToolParams = { pattern: 'hello' };1197      const invocation = multiDirGrepTool.build(params);1198      const result = await invocation.execute(abortSignal);1199 1200      // Despite two identical lines in the raw output, only 1 match should be reported.

Showing the first 1,200 of 1535 lines. Download the file for the rest.

basant307/AI_Governance_Project · CoolFace