Merge branch 'main' into web-search

This commit is contained in:
pomelo-nwu
2025-11-03 17:34:03 +08:00
36 changed files with 1766 additions and 760 deletions

View File

@@ -154,6 +154,11 @@ vi.mock('../core/tokenLimits.js', () => ({
describe('Server Config (config.ts)', () => {
const MODEL = 'qwen3-coder-plus';
// Default mock for canUseRipgrep to return true (tests that care about ripgrep will override this)
beforeEach(() => {
vi.mocked(canUseRipgrep).mockResolvedValue(true);
});
const SANDBOX: SandboxConfig = {
command: 'docker',
image: 'qwen-code-sandbox',
@@ -578,6 +583,40 @@ describe('Server Config (config.ts)', () => {
});
});
describe('UseBuiltinRipgrep Configuration', () => {
it('should default useBuiltinRipgrep to true when not provided', () => {
const config = new Config(baseParams);
expect(config.getUseBuiltinRipgrep()).toBe(true);
});
it('should set useBuiltinRipgrep to false when provided as false', () => {
const paramsWithBuiltinRipgrep: ConfigParameters = {
...baseParams,
useBuiltinRipgrep: false,
};
const config = new Config(paramsWithBuiltinRipgrep);
expect(config.getUseBuiltinRipgrep()).toBe(false);
});
it('should set useBuiltinRipgrep to true when explicitly provided as true', () => {
const paramsWithBuiltinRipgrep: ConfigParameters = {
...baseParams,
useBuiltinRipgrep: true,
};
const config = new Config(paramsWithBuiltinRipgrep);
expect(config.getUseBuiltinRipgrep()).toBe(true);
});
it('should default useBuiltinRipgrep to true when undefined', () => {
const paramsWithUndefinedBuiltinRipgrep: ConfigParameters = {
...baseParams,
useBuiltinRipgrep: undefined,
};
const config = new Config(paramsWithUndefinedBuiltinRipgrep);
expect(config.getUseBuiltinRipgrep()).toBe(true);
});
});
describe('createToolRegistry', () => {
it('should register a tool if coreTools contains an argument-specific pattern', async () => {
const params: ConfigParameters = {
@@ -825,10 +864,60 @@ describe('setApprovalMode with folder trust', () => {
expect(wasRipGrepRegistered).toBe(true);
expect(wasGrepRegistered).toBe(false);
expect(logRipgrepFallback).not.toHaveBeenCalled();
expect(canUseRipgrep).toHaveBeenCalledWith(true);
});
it('should register GrepTool as a fallback when useRipgrep is true but it is not available', async () => {
it('should register RipGrepTool with system ripgrep when useBuiltinRipgrep is false', async () => {
(canUseRipgrep as Mock).mockResolvedValue(true);
const config = new Config({
...baseParams,
useRipgrep: true,
useBuiltinRipgrep: false,
});
await config.initialize();
const calls = (ToolRegistry.prototype.registerTool as Mock).mock.calls;
const wasRipGrepRegistered = calls.some(
(call) => call[0] instanceof vi.mocked(RipGrepTool),
);
const wasGrepRegistered = calls.some(
(call) => call[0] instanceof vi.mocked(GrepTool),
);
expect(wasRipGrepRegistered).toBe(true);
expect(wasGrepRegistered).toBe(false);
expect(canUseRipgrep).toHaveBeenCalledWith(false);
});
it('should fall back to GrepTool and log error when useBuiltinRipgrep is false but system ripgrep is not available', async () => {
(canUseRipgrep as Mock).mockResolvedValue(false);
const config = new Config({
...baseParams,
useRipgrep: true,
useBuiltinRipgrep: false,
});
await config.initialize();
const calls = (ToolRegistry.prototype.registerTool as Mock).mock.calls;
const wasRipGrepRegistered = calls.some(
(call) => call[0] instanceof vi.mocked(RipGrepTool),
);
const wasGrepRegistered = calls.some(
(call) => call[0] instanceof vi.mocked(GrepTool),
);
expect(wasRipGrepRegistered).toBe(false);
expect(wasGrepRegistered).toBe(true);
expect(canUseRipgrep).toHaveBeenCalledWith(false);
expect(logRipgrepFallback).toHaveBeenCalledWith(
config,
expect.any(RipgrepFallbackEvent),
);
const event = (logRipgrepFallback as Mock).mock.calls[0][1];
expect(event.error).toContain('Ripgrep is not available');
});
it('should fall back to GrepTool and log error when useRipgrep is true and builtin ripgrep is not available', async () => {
(canUseRipgrep as Mock).mockResolvedValue(false);
const config = new Config({ ...baseParams, useRipgrep: true });
await config.initialize();
@@ -843,15 +932,16 @@ describe('setApprovalMode with folder trust', () => {
expect(wasRipGrepRegistered).toBe(false);
expect(wasGrepRegistered).toBe(true);
expect(canUseRipgrep).toHaveBeenCalledWith(true);
expect(logRipgrepFallback).toHaveBeenCalledWith(
config,
expect.any(RipgrepFallbackEvent),
);
const event = (logRipgrepFallback as Mock).mock.calls[0][1];
expect(event.error).toBeUndefined();
expect(event.error).toContain('Ripgrep is not available');
});
it('should register GrepTool as a fallback when canUseRipgrep throws an error', async () => {
it('should fall back to GrepTool and log error when canUseRipgrep throws an error', async () => {
const error = new Error('ripGrep check failed');
(canUseRipgrep as Mock).mockRejectedValue(error);
const config = new Config({ ...baseParams, useRipgrep: true });
@@ -890,7 +980,6 @@ describe('setApprovalMode with folder trust', () => {
expect(wasRipGrepRegistered).toBe(false);
expect(wasGrepRegistered).toBe(true);
expect(canUseRipgrep).not.toHaveBeenCalled();
expect(logRipgrepFallback).not.toHaveBeenCalled();
});
});
});

View File

@@ -274,6 +274,7 @@ export interface ConfigParameters {
interactive?: boolean;
trustedFolder?: boolean;
useRipgrep?: boolean;
useBuiltinRipgrep?: boolean;
shouldUseNodePtyShell?: boolean;
skipNextSpeakerCheck?: boolean;
shellExecutionConfig?: ShellExecutionConfig;
@@ -369,6 +370,7 @@ export class Config {
private readonly interactive: boolean;
private readonly trustedFolder: boolean | undefined;
private readonly useRipgrep: boolean;
private readonly useBuiltinRipgrep: boolean;
private readonly shouldUseNodePtyShell: boolean;
private readonly skipNextSpeakerCheck: boolean;
private shellExecutionConfig: ShellExecutionConfig;
@@ -466,13 +468,12 @@ export class Config {
this.chatCompression = params.chatCompression;
this.interactive = params.interactive ?? false;
this.trustedFolder = params.trustedFolder;
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? false;
this.skipLoopDetection = params.skipLoopDetection ?? false;
// Web search
this.webSearch = params.webSearch;
this.useRipgrep = params.useRipgrep ?? true;
this.useBuiltinRipgrep = params.useBuiltinRipgrep ?? true;
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true;
this.shellExecutionConfig = {
@@ -1002,6 +1003,10 @@ export class Config {
return this.useRipgrep;
}
getUseBuiltinRipgrep(): boolean {
return this.useBuiltinRipgrep;
}
getShouldUseNodePtyShell(): boolean {
return this.shouldUseNodePtyShell;
}
@@ -1129,13 +1134,18 @@ export class Config {
let useRipgrep = false;
let errorString: undefined | string = undefined;
try {
useRipgrep = await canUseRipgrep();
useRipgrep = await canUseRipgrep(this.getUseBuiltinRipgrep());
} catch (error: unknown) {
errorString = String(error);
}
if (useRipgrep) {
registerCoreTool(RipGrepTool, this);
} else {
errorString =
errorString ||
'Ripgrep is not available. Please install ripgrep globally.';
// Log for telemetry
logRipgrepFallback(this, new RipgrepFallbackEvent(errorString));
registerCoreTool(GrepTool, this);
}