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 { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
|
||||||
import type { ChildProcess } from 'node:child_process';
|
import type { ChildProcess } from 'node:child_process';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||||
|
|
||||||
// Mock @lvce-editor/ripgrep for testing
|
// Mock @lvce-editor/ripgrep for testing
|
||||||
vi.mock('@lvce-editor/ripgrep', () => ({
|
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', () => {
|
describe('RipGrepTool', () => {
|
||||||
let tempRootDir: string;
|
let tempRootDir: string;
|
||||||
let grepTool: RipGrepTool;
|
let grepTool: RipGrepTool;
|
||||||
|
let mockConfig: Config;
|
||||||
const abortSignal = new AbortController().signal;
|
const abortSignal = new AbortController().signal;
|
||||||
|
|
||||||
const mockConfig = {
|
|
||||||
getTargetDir: () => tempRootDir,
|
|
||||||
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
|
||||||
getDebugMode: () => false,
|
|
||||||
} as unknown as Config;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockSpawn.mockClear();
|
mockSpawn.mockClear();
|
||||||
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
|
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
|
||||||
|
mockConfig = createTestConfig(tempRootDir);
|
||||||
grepTool = new RipGrepTool(mockConfig);
|
grepTool = new RipGrepTool(mockConfig);
|
||||||
|
|
||||||
// Create some test files and directories
|
// Create some test files and directories
|
||||||
@@ -293,6 +308,42 @@ describe('RipGrepTool', () => {
|
|||||||
expect(result.returnDisplay).toBe('Found 1 match');
|
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 () => {
|
it('should return "No matches found" when pattern does not exist', async () => {
|
||||||
// Setup specific mock for no matches
|
// Setup specific mock for no matches
|
||||||
mockSpawn.mockImplementationOnce(
|
mockSpawn.mockImplementationOnce(
|
||||||
@@ -452,12 +503,7 @@ describe('RipGrepTool', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create a mock config with multiple directories
|
// Create a mock config with multiple directories
|
||||||
const multiDirConfig = {
|
const multiDirConfig = createTestConfig(tempRootDir, [secondDir]);
|
||||||
getTargetDir: () => tempRootDir,
|
|
||||||
getWorkspaceContext: () =>
|
|
||||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
|
||||||
getDebugMode: () => false,
|
|
||||||
} as unknown as Config;
|
|
||||||
|
|
||||||
// Setup specific mock for this test - multi-directory search for 'world'
|
// Setup specific mock for this test - multi-directory search for 'world'
|
||||||
// Mock will be called twice - once for each directory
|
// Mock will be called twice - once for each directory
|
||||||
@@ -557,12 +603,7 @@ describe('RipGrepTool', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create a mock config with multiple directories
|
// Create a mock config with multiple directories
|
||||||
const multiDirConfig = {
|
const multiDirConfig = createTestConfig(tempRootDir, [secondDir]);
|
||||||
getTargetDir: () => tempRootDir,
|
|
||||||
getWorkspaceContext: () =>
|
|
||||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
|
||||||
getDebugMode: () => false,
|
|
||||||
} as unknown as Config;
|
|
||||||
|
|
||||||
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
||||||
mockSpawn.mockImplementationOnce(() => {
|
mockSpawn.mockImplementationOnce(() => {
|
||||||
@@ -1187,12 +1228,7 @@ describe('RipGrepTool', () => {
|
|||||||
|
|
||||||
it('should indicate searching across all workspace directories when no path specified', () => {
|
it('should indicate searching across all workspace directories when no path specified', () => {
|
||||||
// Create a mock config with multiple directories
|
// Create a mock config with multiple directories
|
||||||
const multiDirConfig = {
|
const multiDirConfig = createTestConfig(tempRootDir, ['/another/dir']);
|
||||||
getTargetDir: () => tempRootDir,
|
|
||||||
getWorkspaceContext: () =>
|
|
||||||
createMockWorkspaceContext(tempRootDir, ['/another/dir']),
|
|
||||||
getDebugMode: () => false,
|
|
||||||
} as unknown as Config;
|
|
||||||
|
|
||||||
const multiDirGrepTool = new RipGrepTool(multiDirConfig);
|
const multiDirGrepTool = new RipGrepTool(multiDirConfig);
|
||||||
const params: RipGrepToolParams = { pattern: 'testPattern' };
|
const params: RipGrepToolParams = { pattern: 'testPattern' };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { EOL } from 'node:os';
|
import { EOL, tmpdir } from 'node:os';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import type { ToolInvocation, ToolResult } from './tools.js';
|
import type { ToolInvocation, ToolResult } from './tools.js';
|
||||||
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
|
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
|
||||||
@@ -58,6 +58,8 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
RipGrepToolParams,
|
RipGrepToolParams,
|
||||||
ToolResult
|
ToolResult
|
||||||
> {
|
> {
|
||||||
|
private readonly tempIgnoreFiles: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
params: RipGrepToolParams,
|
params: RipGrepToolParams,
|
||||||
@@ -109,9 +111,28 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||||
try {
|
try {
|
||||||
const workspaceContext = this.config.getWorkspaceContext();
|
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 searchDirAbs = this.resolveAndValidatePath(this.params.path);
|
||||||
const searchDirDisplay = 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
|
// Determine which directories to search
|
||||||
let searchDirectories: readonly string[];
|
let searchDirectories: readonly string[];
|
||||||
if (searchDirAbs === null) {
|
if (searchDirAbs === null) {
|
||||||
@@ -125,26 +146,37 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
let allMatches: GrepMatch[] = [];
|
let allMatches: GrepMatch[] = [];
|
||||||
const totalMaxMatches = DEFAULT_TOTAL_MAX_MATCHES;
|
const totalMaxMatches = DEFAULT_TOTAL_MAX_MATCHES;
|
||||||
|
|
||||||
if (this.config.getDebugMode()) {
|
|
||||||
console.log(`[GrepTool] Total result limit: ${totalMaxMatches}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const searchDir of searchDirectories) {
|
for (const searchDir of searchDirectories) {
|
||||||
const searchResult = await this.performRipgrepSearch({
|
const searchResult = await this.performRipgrepSearch({
|
||||||
pattern: this.params.pattern,
|
pattern: this.params.pattern,
|
||||||
path: searchDir,
|
path: searchDir,
|
||||||
include: this.params.include,
|
include: this.params.include,
|
||||||
signal,
|
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) {
|
if (searchDirectories.length > 1) {
|
||||||
const dirName = path.basename(searchDir);
|
const dirName = path.basename(searchDir);
|
||||||
searchResult.forEach((match) => {
|
filteredMatches.forEach((match) => {
|
||||||
match.filePath = path.join(dirName, match.filePath);
|
match.filePath = path.join(dirName, match.filePath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
allMatches = allMatches.concat(searchResult);
|
allMatches = allMatches.concat(filteredMatches);
|
||||||
|
|
||||||
if (allMatches.length >= totalMaxMatches) {
|
if (allMatches.length >= totalMaxMatches) {
|
||||||
allMatches = allMatches.slice(0, totalMaxMatches);
|
allMatches = allMatches.slice(0, totalMaxMatches);
|
||||||
@@ -219,6 +251,8 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
llmContent: `Error during grep search operation: ${errorMessage}`,
|
llmContent: `Error during grep search operation: ${errorMessage}`,
|
||||||
returnDisplay: `Error: ${errorMessage}`,
|
returnDisplay: `Error: ${errorMessage}`,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
this.cleanupTemporaryIgnoreFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,8 +299,9 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
path: string;
|
path: string;
|
||||||
include?: string;
|
include?: string;
|
||||||
signal: AbortSignal;
|
signal: AbortSignal;
|
||||||
|
ignoreFiles?: string[];
|
||||||
}): Promise<GrepMatch[]> {
|
}): Promise<GrepMatch[]> {
|
||||||
const { pattern, path: absolutePath, include } = options;
|
const { pattern, path: absolutePath, include, ignoreFiles } = options;
|
||||||
|
|
||||||
const rgArgs = [
|
const rgArgs = [
|
||||||
'--line-number',
|
'--line-number',
|
||||||
@@ -281,6 +316,12 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
rgArgs.push('--glob', include);
|
rgArgs.push('--glob', include);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoreFiles && ignoreFiles.length > 0) {
|
||||||
|
for (const ignoreFile of ignoreFiles) {
|
||||||
|
rgArgs.push('--ignore-file', ignoreFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const excludes = [
|
const excludes = [
|
||||||
'.git',
|
'.git',
|
||||||
'node_modules',
|
'node_modules',
|
||||||
@@ -389,6 +430,43 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
return description;
|
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[]) {
|
private addPatterns(patterns: string[]) {
|
||||||
this.ig.add(patterns);
|
const normalizedPatterns = patterns.map((pattern) => {
|
||||||
this.patterns.push(...patterns);
|
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 {
|
isIgnored(filePath: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user