@@ -14,7 +14,7 @@ import {
1414 type Mock ,
1515} from 'vitest' ;
1616import type { RipGrepToolParams } from './ripGrep.js' ;
17- import { canUseRipgrep , RipGrepTool } from './ripGrep.js' ;
17+ import { canUseRipgrep , RipGrepTool , ensureRgPath } from './ripGrep.js' ;
1818import path from 'node:path' ;
1919import fs from 'node:fs/promises' ;
2020import os , { EOL } from 'node:os' ;
@@ -97,6 +97,49 @@ describe('canUseRipgrep', () => {
9797 } ) ;
9898} ) ;
9999
100+ describe ( 'ensureRgPath' , ( ) => {
101+ beforeEach ( ( ) => {
102+ vi . clearAllMocks ( ) ;
103+ } ) ;
104+
105+ it ( 'should return rg path if ripgrep already exists' , async ( ) => {
106+ ( fileExists as Mock ) . mockResolvedValue ( true ) ;
107+ const rgPath = await ensureRgPath ( ) ;
108+ expect ( rgPath ) . toBe ( path . join ( '/mock/bin/dir' , 'rg' ) ) ;
109+ expect ( fileExists ) . toHaveBeenCalledOnce ( ) ;
110+ expect ( downloadRipGrep ) . not . toHaveBeenCalled ( ) ;
111+ } ) ;
112+
113+ it ( 'should return rg path if ripgrep is downloaded successfully' , async ( ) => {
114+ ( fileExists as Mock )
115+ . mockResolvedValueOnce ( false )
116+ . mockResolvedValueOnce ( true ) ;
117+ ( downloadRipGrep as Mock ) . mockResolvedValue ( undefined ) ;
118+ const rgPath = await ensureRgPath ( ) ;
119+ expect ( rgPath ) . toBe ( path . join ( '/mock/bin/dir' , 'rg' ) ) ;
120+ expect ( downloadRipGrep ) . toHaveBeenCalledOnce ( ) ;
121+ expect ( fileExists ) . toHaveBeenCalledTimes ( 2 ) ;
122+ } ) ;
123+
124+ it ( 'should throw an error if ripgrep cannot be used after download attempt' , async ( ) => {
125+ ( fileExists as Mock ) . mockResolvedValue ( false ) ;
126+ ( downloadRipGrep as Mock ) . mockResolvedValue ( undefined ) ;
127+ await expect ( ensureRgPath ( ) ) . rejects . toThrow ( 'Cannot use ripgrep.' ) ;
128+ expect ( downloadRipGrep ) . toHaveBeenCalledOnce ( ) ;
129+ expect ( fileExists ) . toHaveBeenCalledTimes ( 2 ) ;
130+ } ) ;
131+
132+ it ( 'should propagate errors from downloadRipGrep' , async ( ) => {
133+ const error = new Error ( 'Download failed' ) ;
134+ ( fileExists as Mock ) . mockResolvedValue ( false ) ;
135+ ( downloadRipGrep as Mock ) . mockRejectedValue ( error ) ;
136+
137+ await expect ( ensureRgPath ( ) ) . rejects . toThrow ( error ) ;
138+ expect ( fileExists ) . toHaveBeenCalledTimes ( 1 ) ;
139+ expect ( downloadRipGrep ) . toHaveBeenCalledWith ( '/mock/bin/dir' ) ;
140+ } ) ;
141+ } ) ;
142+
100143// Helper function to create mock spawn implementations
101144function createMockSpawn (
102145 options : {
@@ -158,6 +201,8 @@ describe('RipGrepTool', () => {
158201
159202 beforeEach ( async ( ) => {
160203 vi . clearAllMocks ( ) ;
204+ ( downloadRipGrep as Mock ) . mockResolvedValue ( undefined ) ;
205+ ( fileExists as Mock ) . mockResolvedValue ( true ) ;
161206 mockSpawn . mockClear ( ) ;
162207 tempRootDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'grep-tool-root-' ) ) ;
163208 grepTool = new RipGrepTool ( mockConfig ) ;
@@ -504,6 +549,20 @@ describe('RipGrepTool', () => {
504549 / p a r a m s m u s t h a v e r e q u i r e d p r o p e r t y ' p a t t e r n ' / ,
505550 ) ;
506551 } ) ;
552+
553+ it ( 'should throw an error if ripgrep is not available' , async ( ) => {
554+ // Make ensureRgPath throw
555+ ( fileExists as Mock ) . mockResolvedValue ( false ) ;
556+ ( downloadRipGrep as Mock ) . mockResolvedValue ( undefined ) ;
557+
558+ const params : RipGrepToolParams = { pattern : 'world' } ;
559+ const invocation = grepTool . build ( params ) ;
560+
561+ expect ( await invocation . execute ( abortSignal ) ) . toStrictEqual ( {
562+ llmContent : 'Error during grep search operation: Cannot use ripgrep.' ,
563+ returnDisplay : 'Error: Cannot use ripgrep.' ,
564+ } ) ;
565+ } ) ;
507566 } ) ;
508567
509568 describe ( 'multi-directory workspace' , ( ) => {
0 commit comments