Skip to content

Commit f25e924

Browse files
fix(core): perform spawn check on ripgrep before registration
1 parent 9e5599c commit f25e924

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

packages/core/src/config/config.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
2525
import { debugLogger } from '../utils/debugLogger.js';
2626
import { coreEvents } from '../utils/events.js';
2727
import { ApprovalMode } from '../policy/types.js';
28+
import { execFileSync } from 'node:child_process';
2829
import {
2930
HookType,
3031
HookEventName,
@@ -154,6 +155,14 @@ vi.mock('../tools/memoryTool', async (importOriginal) => {
154155

155156
vi.mock('../core/contentGenerator.js');
156157

158+
vi.mock('node:child_process', async (importOriginal) => {
159+
const actual = await importOriginal<typeof import('node:child_process')>();
160+
return {
161+
...actual,
162+
execFileSync: vi.fn(),
163+
};
164+
});
165+
157166
vi.mock('../core/client.js', () => ({
158167
GeminiClient: vi.fn().mockImplementation(() => ({
159168
initialize: vi.fn().mockResolvedValue(undefined),
@@ -2377,6 +2386,33 @@ describe('setApprovalMode with folder trust', () => {
23772386
expect(event.error).toBe(String(error));
23782387
});
23792388

2389+
it('should register GrepTool as a fallback when the ripgrep binary execution check throws an error', async () => {
2390+
vi.mocked(resolveRipgrepPath).mockResolvedValue('/mock/rg');
2391+
const error = new Error('spawn EFTYPE');
2392+
vi.mocked(execFileSync).mockImplementation(() => {
2393+
throw error;
2394+
});
2395+
const config = new Config({ ...baseParams, useRipgrep: true });
2396+
await config.initialize();
2397+
2398+
const calls = vi.mocked(ToolRegistry.prototype.registerTool).mock.calls;
2399+
const wasRipGrepRegistered = calls.some(
2400+
(call) => call[0] instanceof vi.mocked(RipGrepTool),
2401+
);
2402+
const wasGrepRegistered = calls.some(
2403+
(call) => call[0] instanceof vi.mocked(GrepTool),
2404+
);
2405+
2406+
expect(wasRipGrepRegistered).toBe(false);
2407+
expect(wasGrepRegistered).toBe(true);
2408+
expect(logRipgrepFallback).toHaveBeenCalledWith(
2409+
config,
2410+
expect.any(RipgrepFallbackEvent),
2411+
);
2412+
const event = vi.mocked(logRipgrepFallback).mock.calls[0][1];
2413+
expect(event.error).toBe(String(error));
2414+
});
2415+
23802416
it('should register GrepTool when useRipgrep is false', async () => {
23812417
const config = new Config({ ...baseParams, useRipgrep: false });
23822418
await config.initialize();

packages/core/src/config/config.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as path from 'node:path';
99
import { SandboxPolicyManager } from '../policy/sandboxPolicyManager.js';
1010
import { inspect } from 'node:util';
1111
import process from 'node:process';
12+
import { execFileSync } from 'node:child_process';
1213
import { z } from 'zod';
1314
import type { ConversationRecord } from '../services/chatRecordingService.js';
1415
import type {
@@ -2192,7 +2193,20 @@ export class Config implements McpContext, AgentLoopContext {
21922193
* Checks if ripgrep is available.
21932194
*/
21942195
async canUseRipgrep(): Promise<boolean> {
2195-
return (await this.getRipgrepPath()) !== null;
2196+
const rgPath = await this.getRipgrepPath();
2197+
if (rgPath === null) {
2198+
return false;
2199+
}
2200+
try {
2201+
execFileSync(rgPath, ['--version'], { stdio: 'ignore', timeout: 1000 });
2202+
return true;
2203+
} catch (error: unknown) {
2204+
debugLogger.warn(
2205+
`Ripgrep binary execution check failed at "${rgPath}":`,
2206+
error,
2207+
);
2208+
throw error;
2209+
}
21962210
}
21972211

21982212
/**

0 commit comments

Comments
 (0)