mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
try fix
This commit is contained in:
@@ -14,6 +14,7 @@ import type { Config } from '../config/config.js';
|
||||
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
|
||||
// Mock @lvce-editor/ripgrep for testing
|
||||
vi.mock('@lvce-editor/ripgrep', () => ({
|
||||
@@ -75,21 +76,35 @@ function createMockSpawn(
|
||||
};
|
||||
}
|
||||
|
||||
function createTestConfig(
|
||||
rootDir: string,
|
||||
extraDirectories: string[] = [],
|
||||
): Config {
|
||||
const fileService = new FileDiscoveryService(rootDir);
|
||||
return {
|
||||
getTargetDir: () => rootDir,
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(rootDir, extraDirectories),
|
||||
getDebugMode: () => false,
|
||||
getFileService: () => fileService,
|
||||
getFileFilteringOptions: () => ({
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
}),
|
||||
} as unknown as Config;
|
||||
}
|
||||
|
||||
describe('RipGrepTool', () => {
|
||||
let tempRootDir: string;
|
||||
let grepTool: RipGrepTool;
|
||||
let mockConfig: Config;
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
const mockConfig = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
||||
getDebugMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
mockSpawn.mockClear();
|
||||
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
|
||||
mockConfig = createTestConfig(tempRootDir);
|
||||
grepTool = new RipGrepTool(mockConfig);
|
||||
|
||||
// Create some test files and directories
|
||||
@@ -293,6 +308,42 @@ describe('RipGrepTool', () => {
|
||||
expect(result.returnDisplay).toBe('Found 1 match');
|
||||
});
|
||||
|
||||
it('should filter out matches ignored by .qwenignore', async () => {
|
||||
await fs.writeFile(
|
||||
path.join(tempRootDir, '.qwenignore'),
|
||||
'logs/\n',
|
||||
'utf8',
|
||||
);
|
||||
await fs.mkdir(path.join(tempRootDir, 'logs'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(tempRootDir, 'logs', 'ignored.txt'),
|
||||
'Got it. Thanks for the context!',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData: `logs/ignored.txt:1:Got it. Thanks for the context!${EOL}`,
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const params: RipGrepToolParams = {
|
||||
pattern: 'Got it\\. Thanks for the context!',
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.returnDisplay).toBe('No matches found');
|
||||
expect(result.llmContent).toContain(
|
||||
'No matches found for pattern "Got it\\. Thanks for the context!" in the workspace directory',
|
||||
);
|
||||
|
||||
const spawnArgs = mockSpawn.mock.calls[0]?.[1] ?? [];
|
||||
expect(spawnArgs).toContain('--ignore-file');
|
||||
expect(spawnArgs).toContain(path.join(tempRootDir, '.qwenignore'));
|
||||
});
|
||||
|
||||
it('should return "No matches found" when pattern does not exist', async () => {
|
||||
// Setup specific mock for no matches
|
||||
mockSpawn.mockImplementationOnce(
|
||||
@@ -452,12 +503,7 @@ describe('RipGrepTool', () => {
|
||||
);
|
||||
|
||||
// Create a mock config with multiple directories
|
||||
const multiDirConfig = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||
getDebugMode: () => false,
|
||||
} as unknown as Config;
|
||||
const multiDirConfig = createTestConfig(tempRootDir, [secondDir]);
|
||||
|
||||
// Setup specific mock for this test - multi-directory search for 'world'
|
||||
// Mock will be called twice - once for each directory
|
||||
@@ -557,12 +603,7 @@ describe('RipGrepTool', () => {
|
||||
);
|
||||
|
||||
// Create a mock config with multiple directories
|
||||
const multiDirConfig = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||
getDebugMode: () => false,
|
||||
} as unknown as Config;
|
||||
const multiDirConfig = createTestConfig(tempRootDir, [secondDir]);
|
||||
|
||||
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
||||
mockSpawn.mockImplementationOnce(() => {
|
||||
@@ -1187,12 +1228,7 @@ describe('RipGrepTool', () => {
|
||||
|
||||
it('should indicate searching across all workspace directories when no path specified', () => {
|
||||
// Create a mock config with multiple directories
|
||||
const multiDirConfig = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(tempRootDir, ['/another/dir']),
|
||||
getDebugMode: () => false,
|
||||
} as unknown as Config;
|
||||
const multiDirConfig = createTestConfig(tempRootDir, ['/another/dir']);
|
||||
|
||||
const multiDirGrepTool = new RipGrepTool(multiDirConfig);
|
||||
const params: RipGrepToolParams = { pattern: 'testPattern' };
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { EOL } from 'node:os';
|
||||
import { EOL, tmpdir } from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import type { ToolInvocation, ToolResult } from './tools.js';
|
||||
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
|
||||
@@ -58,6 +58,8 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
RipGrepToolParams,
|
||||
ToolResult
|
||||
> {
|
||||
private readonly tempIgnoreFiles: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
params: RipGrepToolParams,
|
||||
@@ -109,9 +111,28 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
try {
|
||||
const workspaceContext = this.config.getWorkspaceContext();
|
||||
const fileService = this.config.getFileService();
|
||||
const fileFilteringOptions = this.config.getFileFilteringOptions();
|
||||
const qwenIgnoreFiles =
|
||||
fileFilteringOptions.respectGeminiIgnore === false
|
||||
? []
|
||||
: this.getQwenIgnoreFilePaths();
|
||||
const searchDirAbs = this.resolveAndValidatePath(this.params.path);
|
||||
const searchDirDisplay = this.params.path || '.';
|
||||
|
||||
// if (this.config.getDebugMode()) {
|
||||
console.log(
|
||||
`[GrepTool] Using qwenignore files: ${
|
||||
qwenIgnoreFiles.length > 0
|
||||
? qwenIgnoreFiles.join(', ')
|
||||
: 'none (qwenignore disabled or file missing)'
|
||||
}`,
|
||||
);
|
||||
console.log(
|
||||
`[GrepTool] File filtering: respectGitIgnore=${fileFilteringOptions.respectGitIgnore ?? true}, respectQwenIgnore=${fileFilteringOptions.respectGeminiIgnore ?? true}`,
|
||||
);
|
||||
// }
|
||||
|
||||
// Determine which directories to search
|
||||
let searchDirectories: readonly string[];
|
||||
if (searchDirAbs === null) {
|
||||
@@ -125,26 +146,37 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
let allMatches: GrepMatch[] = [];
|
||||
const totalMaxMatches = DEFAULT_TOTAL_MAX_MATCHES;
|
||||
|
||||
if (this.config.getDebugMode()) {
|
||||
console.log(`[GrepTool] Total result limit: ${totalMaxMatches}`);
|
||||
}
|
||||
|
||||
for (const searchDir of searchDirectories) {
|
||||
const searchResult = await this.performRipgrepSearch({
|
||||
pattern: this.params.pattern,
|
||||
path: searchDir,
|
||||
include: this.params.include,
|
||||
signal,
|
||||
ignoreFiles: qwenIgnoreFiles,
|
||||
});
|
||||
|
||||
let filteredMatches = searchResult;
|
||||
if (
|
||||
fileFilteringOptions.respectGitIgnore ||
|
||||
fileFilteringOptions.respectGeminiIgnore
|
||||
) {
|
||||
filteredMatches = searchResult.filter((match) => {
|
||||
const absoluteMatchPath = path.resolve(searchDir, match.filePath);
|
||||
return !fileService.shouldIgnoreFile(
|
||||
absoluteMatchPath,
|
||||
fileFilteringOptions,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (searchDirectories.length > 1) {
|
||||
const dirName = path.basename(searchDir);
|
||||
searchResult.forEach((match) => {
|
||||
filteredMatches.forEach((match) => {
|
||||
match.filePath = path.join(dirName, match.filePath);
|
||||
});
|
||||
}
|
||||
|
||||
allMatches = allMatches.concat(searchResult);
|
||||
allMatches = allMatches.concat(filteredMatches);
|
||||
|
||||
if (allMatches.length >= totalMaxMatches) {
|
||||
allMatches = allMatches.slice(0, totalMaxMatches);
|
||||
@@ -219,6 +251,8 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
llmContent: `Error during grep search operation: ${errorMessage}`,
|
||||
returnDisplay: `Error: ${errorMessage}`,
|
||||
};
|
||||
} finally {
|
||||
this.cleanupTemporaryIgnoreFiles();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,8 +299,9 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
path: string;
|
||||
include?: string;
|
||||
signal: AbortSignal;
|
||||
ignoreFiles?: string[];
|
||||
}): Promise<GrepMatch[]> {
|
||||
const { pattern, path: absolutePath, include } = options;
|
||||
const { pattern, path: absolutePath, include, ignoreFiles } = options;
|
||||
|
||||
const rgArgs = [
|
||||
'--line-number',
|
||||
@@ -281,6 +316,12 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
rgArgs.push('--glob', include);
|
||||
}
|
||||
|
||||
if (ignoreFiles && ignoreFiles.length > 0) {
|
||||
for (const ignoreFile of ignoreFiles) {
|
||||
rgArgs.push('--ignore-file', ignoreFile);
|
||||
}
|
||||
}
|
||||
|
||||
const excludes = [
|
||||
'.git',
|
||||
'node_modules',
|
||||
@@ -389,6 +430,43 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
private getQwenIgnoreFilePaths(): string[] {
|
||||
const patterns = this.config.getFileService().getGeminiIgnorePatterns();
|
||||
if (patterns.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tempFilePath = path.join(
|
||||
tmpdir(),
|
||||
`qwen-ignore-${process.pid}-${Date.now()}-${Math.random()
|
||||
.toString(16)
|
||||
.slice(2)}.rgignore`,
|
||||
);
|
||||
|
||||
try {
|
||||
const fileContents = `${patterns.join(EOL)}${EOL}`;
|
||||
fs.writeFileSync(tempFilePath, fileContents, 'utf8');
|
||||
this.tempIgnoreFiles.push(tempFilePath);
|
||||
return [tempFilePath];
|
||||
} catch (error: unknown) {
|
||||
console.warn(
|
||||
`Failed to create temporary .qwenignore for ripgrep: ${getErrorMessage(error)}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupTemporaryIgnoreFiles(): void {
|
||||
for (const filePath of this.tempIgnoreFiles) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
this.tempIgnoreFiles.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,8 +52,38 @@ export class GitIgnoreParser implements GitIgnoreFilter {
|
||||
}
|
||||
|
||||
private addPatterns(patterns: string[]) {
|
||||
this.ig.add(patterns);
|
||||
this.patterns.push(...patterns);
|
||||
const normalizedPatterns = patterns.map((pattern) => {
|
||||
if (!pattern) {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
if (path.isAbsolute(pattern)) {
|
||||
const relativePattern = path.relative(this.projectRoot, pattern);
|
||||
|
||||
if (relativePattern === '' || relativePattern === '.') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (!relativePattern.startsWith('..')) {
|
||||
let normalized = relativePattern.replace(/\\/g, '/');
|
||||
|
||||
if (pattern.endsWith('/') && !normalized.endsWith('/')) {
|
||||
normalized += '/';
|
||||
}
|
||||
|
||||
if (!normalized.startsWith('/')) {
|
||||
normalized = `/${normalized}`;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
});
|
||||
|
||||
this.ig.add(normalizedPatterns);
|
||||
this.patterns.push(...normalizedPatterns);
|
||||
}
|
||||
|
||||
isIgnored(filePath: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user