@google/gemini-cli-core 0.2.0-preview.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -32
- package/dist/google-gemini-cli-core-0.2.0-preview.2.tgz +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/code_assist/oauth2.js +15 -9
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.test.js +4 -6
- package/dist/src/code_assist/oauth2.test.js.map +1 -1
- package/dist/src/config/config.d.ts +2 -12
- package/dist/src/config/config.js +11 -30
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +2 -33
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/core/client.js +3 -0
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +61 -0
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/contentGenerator.js +2 -3
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +3 -2
- package/dist/src/core/coreToolScheduler.js +1 -1
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/coreToolScheduler.test.js +11 -10
- package/dist/src/core/coreToolScheduler.test.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +9 -4
- package/dist/src/core/geminiChat.js +161 -168
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/geminiChat.test.js +296 -2
- package/dist/src/core/geminiChat.test.js.map +1 -1
- package/dist/src/core/logger.d.ts +1 -3
- package/dist/src/core/logger.js +3 -4
- package/dist/src/core/logger.js.map +1 -1
- package/dist/src/core/logger.test.js +16 -17
- package/dist/src/core/logger.test.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.d.ts +5 -3
- package/dist/src/core/nonInteractiveToolExecutor.js +122 -14
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.test.js +53 -56
- package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
- package/dist/src/core/turn.js +4 -4
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/core/turn.test.js +0 -19
- package/dist/src/core/turn.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/ide/ide-client.d.ts +2 -4
- package/dist/src/ide/ide-client.js +16 -81
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/ide/ide-client.test.js +33 -141
- package/dist/src/ide/ide-client.test.js.map +1 -1
- package/dist/src/ide/ideContext.d.ts +5 -5
- package/dist/src/index.d.ts +0 -3
- package/dist/src/index.js +0 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.d.ts +2 -0
- package/dist/src/mcp/oauth-token-storage.js +5 -2
- package/dist/src/mcp/oauth-token-storage.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.test.js +0 -1
- package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
- package/dist/src/services/chatRecordingService.js +2 -2
- package/dist/src/services/chatRecordingService.js.map +1 -1
- package/dist/src/services/chatRecordingService.test.js +3 -5
- package/dist/src/services/chatRecordingService.test.js.map +1 -1
- package/dist/src/services/gitService.d.ts +1 -3
- package/dist/src/services/gitService.js +10 -19
- package/dist/src/services/gitService.js.map +1 -1
- package/dist/src/services/gitService.test.js +14 -17
- package/dist/src/services/gitService.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +9 -7
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +59 -75
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +9 -66
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +20 -7
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +46 -11
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/constants.d.ts +6 -0
- package/dist/src/telemetry/constants.js +6 -0
- package/dist/src/telemetry/constants.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +4 -2
- package/dist/src/telemetry/loggers.js +56 -11
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +0 -4
- package/dist/src/telemetry/loggers.test.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +13 -1
- package/dist/src/telemetry/metrics.js +43 -5
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +23 -19
- package/dist/src/telemetry/types.js +36 -33
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/edit.js +4 -25
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/edit.test.js +0 -24
- package/dist/src/tools/edit.test.js.map +1 -1
- package/dist/src/tools/glob.js +2 -13
- package/dist/src/tools/glob.js.map +1 -1
- package/dist/src/tools/glob.test.js +1 -23
- package/dist/src/tools/glob.test.js.map +1 -1
- package/dist/src/tools/grep.js +0 -5
- package/dist/src/tools/grep.js.map +1 -1
- package/dist/src/tools/grep.test.js +0 -11
- package/dist/src/tools/grep.test.js.map +1 -1
- package/dist/src/tools/ls.js +4 -9
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/ls.test.js +0 -5
- package/dist/src/tools/ls.test.js.map +1 -1
- package/dist/src/tools/mcp-client.d.ts +0 -7
- package/dist/src/tools/mcp-client.js +21 -26
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +1 -26
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/mcp-tool.js +2 -12
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/mcp-tool.test.js +3 -20
- package/dist/src/tools/mcp-tool.test.js.map +1 -1
- package/dist/src/tools/memoryTool.js +2 -7
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/memoryTool.test.js +1 -13
- package/dist/src/tools/memoryTool.test.js.map +1 -1
- package/dist/src/tools/read-file.js +32 -10
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/read-file.test.js +2 -5
- package/dist/src/tools/read-file.test.js.map +1 -1
- package/dist/src/tools/read-many-files.js +11 -21
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/read-many-files.test.js +4 -42
- package/dist/src/tools/read-many-files.test.js.map +1 -1
- package/dist/src/tools/shell.js +2 -2
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/shell.test.js +1 -2
- package/dist/src/tools/shell.test.js.map +1 -1
- package/dist/src/tools/tool-error.d.ts +1 -16
- package/dist/src/tools/tool-error.js +0 -24
- package/dist/src/tools/tool-error.js.map +1 -1
- package/dist/src/tools/tool-registry.js +1 -7
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tool-registry.test.js +1 -71
- package/dist/src/tools/tool-registry.test.js.map +1 -1
- package/dist/src/tools/tools.d.ts +5 -1
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/tools.test.js +1 -0
- package/dist/src/tools/tools.test.js.map +1 -1
- package/dist/src/tools/web-fetch.js +6 -9
- package/dist/src/tools/web-fetch.js.map +1 -1
- package/dist/src/tools/web-fetch.test.js +16 -55
- package/dist/src/tools/web-fetch.test.js.map +1 -1
- package/dist/src/tools/web-search.js +5 -26
- package/dist/src/tools/web-search.js.map +1 -1
- package/dist/src/tools/web-search.test.js +1 -69
- package/dist/src/tools/web-search.test.js.map +1 -1
- package/dist/src/tools/write-file.js +3 -7
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/tools/write-file.test.js +0 -3
- package/dist/src/tools/write-file.test.js.map +1 -1
- package/dist/src/utils/editCorrector.test.js +0 -1
- package/dist/src/utils/editCorrector.test.js.map +1 -1
- package/dist/src/utils/editor.js +1 -1
- package/dist/src/utils/editor.js.map +1 -1
- package/dist/src/utils/editor.test.js +1 -1
- package/dist/src/utils/editor.test.js.map +1 -1
- package/dist/src/utils/fileUtils.d.ts +7 -2
- package/dist/src/utils/fileUtils.js +17 -14
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/filesearch/fileSearch.d.ts +0 -1
- package/dist/src/utils/filesearch/fileSearch.js +2 -3
- package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
- package/dist/src/utils/filesearch/fileSearch.test.js +0 -90
- package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.js +20 -54
- package/dist/src/utils/memoryDiscovery.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.test.js +0 -37
- package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
- package/dist/src/utils/paths.d.ts +17 -0
- package/dist/src/utils/paths.js +26 -0
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/src/utils/user_account.d.ts +9 -0
- package/dist/src/utils/user_account.js +109 -0
- package/dist/src/utils/user_account.js.map +1 -0
- package/dist/src/utils/{userAccountManager.test.js → user_account.test.js} +30 -33
- package/dist/src/utils/user_account.test.js.map +1 -0
- package/dist/src/utils/user_id.d.ts +11 -0
- package/dist/src/utils/user_id.js +49 -0
- package/dist/src/utils/user_id.js.map +1 -0
- package/dist/src/utils/user_id.test.js +21 -0
- package/dist/src/utils/user_id.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/src/config/storage.d.ts +0 -32
- package/dist/src/config/storage.js +0 -90
- package/dist/src/config/storage.js.map +0 -1
- package/dist/src/config/storage.test.js +0 -43
- package/dist/src/config/storage.test.js.map +0 -1
- package/dist/src/telemetry/telemetry-utils.d.ts +0 -6
- package/dist/src/telemetry/telemetry-utils.js +0 -14
- package/dist/src/telemetry/telemetry-utils.js.map +0 -1
- package/dist/src/telemetry/telemetry-utils.test.js +0 -40
- package/dist/src/telemetry/telemetry-utils.test.js.map +0 -1
- package/dist/src/tools/ripGrep.d.ts +0 -46
- package/dist/src/tools/ripGrep.js +0 -368
- package/dist/src/tools/ripGrep.js.map +0 -1
- package/dist/src/tools/ripGrep.test.d.ts +0 -6
- package/dist/src/tools/ripGrep.test.js +0 -874
- package/dist/src/tools/ripGrep.test.js.map +0 -1
- package/dist/src/utils/installationManager.d.ts +0 -16
- package/dist/src/utils/installationManager.js +0 -50
- package/dist/src/utils/installationManager.js.map +0 -1
- package/dist/src/utils/installationManager.test.d.ts +0 -6
- package/dist/src/utils/installationManager.test.js +0 -83
- package/dist/src/utils/installationManager.test.js.map +0 -1
- package/dist/src/utils/language-detection.d.ts +0 -6
- package/dist/src/utils/language-detection.js +0 -101
- package/dist/src/utils/language-detection.js.map +0 -1
- package/dist/src/utils/userAccountManager.d.ts +0 -20
- package/dist/src/utils/userAccountManager.js +0 -114
- package/dist/src/utils/userAccountManager.js.map +0 -1
- package/dist/src/utils/userAccountManager.test.d.ts +0 -6
- package/dist/src/utils/userAccountManager.test.js.map +0 -1
- /package/dist/src/{config/storage.test.d.ts → utils/user_account.test.d.ts} +0 -0
- /package/dist/src/{telemetry/telemetry-utils.test.d.ts → utils/user_id.test.d.ts} +0 -0
|
@@ -1,874 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
-
import { RipGrepTool } from './ripGrep.js';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import fs from 'fs/promises';
|
|
10
|
-
import os, { EOL } from 'os';
|
|
11
|
-
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
13
|
-
// Mock @lvce-editor/ripgrep for testing
|
|
14
|
-
vi.mock('@lvce-editor/ripgrep', () => ({
|
|
15
|
-
rgPath: '/mock/rg/path',
|
|
16
|
-
}));
|
|
17
|
-
// Mock child_process for ripgrep calls
|
|
18
|
-
vi.mock('child_process', () => ({
|
|
19
|
-
spawn: vi.fn(),
|
|
20
|
-
}));
|
|
21
|
-
const mockSpawn = vi.mocked(spawn);
|
|
22
|
-
// Helper function to create mock spawn implementations
|
|
23
|
-
function createMockSpawn(options = {}) {
|
|
24
|
-
const { outputData, exitCode = 0, signal } = options;
|
|
25
|
-
return () => {
|
|
26
|
-
const mockProcess = {
|
|
27
|
-
stdout: {
|
|
28
|
-
on: vi.fn(),
|
|
29
|
-
removeListener: vi.fn(),
|
|
30
|
-
},
|
|
31
|
-
stderr: {
|
|
32
|
-
on: vi.fn(),
|
|
33
|
-
removeListener: vi.fn(),
|
|
34
|
-
},
|
|
35
|
-
on: vi.fn(),
|
|
36
|
-
removeListener: vi.fn(),
|
|
37
|
-
kill: vi.fn(),
|
|
38
|
-
};
|
|
39
|
-
// Set up event listeners immediately
|
|
40
|
-
setTimeout(() => {
|
|
41
|
-
const stdoutDataHandler = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
42
|
-
const closeHandler = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
43
|
-
if (stdoutDataHandler && outputData) {
|
|
44
|
-
stdoutDataHandler(Buffer.from(outputData));
|
|
45
|
-
}
|
|
46
|
-
if (closeHandler) {
|
|
47
|
-
closeHandler(exitCode, signal);
|
|
48
|
-
}
|
|
49
|
-
}, 0);
|
|
50
|
-
return mockProcess;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
describe('RipGrepTool', () => {
|
|
54
|
-
let tempRootDir;
|
|
55
|
-
let grepTool;
|
|
56
|
-
const abortSignal = new AbortController().signal;
|
|
57
|
-
const mockConfig = {
|
|
58
|
-
getTargetDir: () => tempRootDir,
|
|
59
|
-
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
|
60
|
-
getDebugMode: () => false,
|
|
61
|
-
};
|
|
62
|
-
beforeEach(async () => {
|
|
63
|
-
vi.clearAllMocks();
|
|
64
|
-
mockSpawn.mockClear();
|
|
65
|
-
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
|
|
66
|
-
grepTool = new RipGrepTool(mockConfig);
|
|
67
|
-
// Create some test files and directories
|
|
68
|
-
await fs.writeFile(path.join(tempRootDir, 'fileA.txt'), 'hello world\nsecond line with world');
|
|
69
|
-
await fs.writeFile(path.join(tempRootDir, 'fileB.js'), 'const foo = "bar";\nfunction baz() { return "hello"; }');
|
|
70
|
-
await fs.mkdir(path.join(tempRootDir, 'sub'));
|
|
71
|
-
await fs.writeFile(path.join(tempRootDir, 'sub', 'fileC.txt'), 'another world in sub dir');
|
|
72
|
-
await fs.writeFile(path.join(tempRootDir, 'sub', 'fileD.md'), '# Markdown file\nThis is a test.');
|
|
73
|
-
});
|
|
74
|
-
afterEach(async () => {
|
|
75
|
-
await fs.rm(tempRootDir, { recursive: true, force: true });
|
|
76
|
-
});
|
|
77
|
-
describe('validateToolParams', () => {
|
|
78
|
-
it('should return null for valid params (pattern only)', () => {
|
|
79
|
-
const params = { pattern: 'hello' };
|
|
80
|
-
expect(grepTool.validateToolParams(params)).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
it('should return null for valid params (pattern and path)', () => {
|
|
83
|
-
const params = { pattern: 'hello', path: '.' };
|
|
84
|
-
expect(grepTool.validateToolParams(params)).toBeNull();
|
|
85
|
-
});
|
|
86
|
-
it('should return null for valid params (pattern, path, and include)', () => {
|
|
87
|
-
const params = {
|
|
88
|
-
pattern: 'hello',
|
|
89
|
-
path: '.',
|
|
90
|
-
include: '*.txt',
|
|
91
|
-
};
|
|
92
|
-
expect(grepTool.validateToolParams(params)).toBeNull();
|
|
93
|
-
});
|
|
94
|
-
it('should return error if pattern is missing', () => {
|
|
95
|
-
const params = { path: '.' };
|
|
96
|
-
expect(grepTool.validateToolParams(params)).toBe(`params must have required property 'pattern'`);
|
|
97
|
-
});
|
|
98
|
-
it('should return null for what would be an invalid regex pattern', () => {
|
|
99
|
-
const params = { pattern: '[[' };
|
|
100
|
-
expect(grepTool.validateToolParams(params)).toBeNull();
|
|
101
|
-
});
|
|
102
|
-
it('should return error if path does not exist', () => {
|
|
103
|
-
const params = {
|
|
104
|
-
pattern: 'hello',
|
|
105
|
-
path: 'nonexistent',
|
|
106
|
-
};
|
|
107
|
-
// Check for the core error message, as the full path might vary
|
|
108
|
-
expect(grepTool.validateToolParams(params)).toContain('Failed to access path stats for');
|
|
109
|
-
expect(grepTool.validateToolParams(params)).toContain('nonexistent');
|
|
110
|
-
});
|
|
111
|
-
it('should return error if path is a file, not a directory', async () => {
|
|
112
|
-
const filePath = path.join(tempRootDir, 'fileA.txt');
|
|
113
|
-
const params = { pattern: 'hello', path: filePath };
|
|
114
|
-
expect(grepTool.validateToolParams(params)).toContain(`Path is not a directory: ${filePath}`);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
describe('execute', () => {
|
|
118
|
-
it('should find matches for a simple pattern in all files', async () => {
|
|
119
|
-
mockSpawn.mockImplementationOnce(createMockSpawn({
|
|
120
|
-
outputData: `fileA.txt:1:hello world${EOL}fileA.txt:2:second line with world${EOL}sub/fileC.txt:1:another world in sub dir${EOL}`,
|
|
121
|
-
exitCode: 0,
|
|
122
|
-
}));
|
|
123
|
-
const params = { pattern: 'world' };
|
|
124
|
-
const invocation = grepTool.build(params);
|
|
125
|
-
const result = await invocation.execute(abortSignal);
|
|
126
|
-
expect(result.llmContent).toContain('Found 3 matches for pattern "world" in the workspace directory');
|
|
127
|
-
expect(result.llmContent).toContain('File: fileA.txt');
|
|
128
|
-
expect(result.llmContent).toContain('L1: hello world');
|
|
129
|
-
expect(result.llmContent).toContain('L2: second line with world');
|
|
130
|
-
expect(result.llmContent).toContain(`File: ${path.join('sub', 'fileC.txt')}`);
|
|
131
|
-
expect(result.llmContent).toContain('L1: another world in sub dir');
|
|
132
|
-
expect(result.returnDisplay).toBe('Found 3 matches');
|
|
133
|
-
});
|
|
134
|
-
it('should find matches in a specific path', async () => {
|
|
135
|
-
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
|
136
|
-
mockSpawn.mockImplementationOnce(createMockSpawn({
|
|
137
|
-
outputData: `fileC.txt:1:another world in sub dir${EOL}`,
|
|
138
|
-
exitCode: 0,
|
|
139
|
-
}));
|
|
140
|
-
const params = { pattern: 'world', path: 'sub' };
|
|
141
|
-
const invocation = grepTool.build(params);
|
|
142
|
-
const result = await invocation.execute(abortSignal);
|
|
143
|
-
expect(result.llmContent).toContain('Found 1 match for pattern "world" in path "sub"');
|
|
144
|
-
expect(result.llmContent).toContain('File: fileC.txt'); // Path relative to 'sub'
|
|
145
|
-
expect(result.llmContent).toContain('L1: another world in sub dir');
|
|
146
|
-
expect(result.returnDisplay).toBe('Found 1 match');
|
|
147
|
-
});
|
|
148
|
-
it('should find matches with an include glob', async () => {
|
|
149
|
-
// Setup specific mock for this test
|
|
150
|
-
mockSpawn.mockImplementationOnce(createMockSpawn({
|
|
151
|
-
outputData: `fileB.js:2:function baz() { return "hello"; }${EOL}`,
|
|
152
|
-
exitCode: 0,
|
|
153
|
-
}));
|
|
154
|
-
const params = { pattern: 'hello', include: '*.js' };
|
|
155
|
-
const invocation = grepTool.build(params);
|
|
156
|
-
const result = await invocation.execute(abortSignal);
|
|
157
|
-
expect(result.llmContent).toContain('Found 1 match for pattern "hello" in the workspace directory (filter: "*.js"):');
|
|
158
|
-
expect(result.llmContent).toContain('File: fileB.js');
|
|
159
|
-
expect(result.llmContent).toContain('L2: function baz() { return "hello"; }');
|
|
160
|
-
expect(result.returnDisplay).toBe('Found 1 match');
|
|
161
|
-
});
|
|
162
|
-
it('should find matches with an include glob and path', async () => {
|
|
163
|
-
await fs.writeFile(path.join(tempRootDir, 'sub', 'another.js'), 'const greeting = "hello";');
|
|
164
|
-
// Setup specific mock for this test - searching for 'hello' in 'sub' with '*.js' filter
|
|
165
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
166
|
-
const mockProcess = {
|
|
167
|
-
stdout: {
|
|
168
|
-
on: vi.fn(),
|
|
169
|
-
removeListener: vi.fn(),
|
|
170
|
-
},
|
|
171
|
-
stderr: {
|
|
172
|
-
on: vi.fn(),
|
|
173
|
-
removeListener: vi.fn(),
|
|
174
|
-
},
|
|
175
|
-
on: vi.fn(),
|
|
176
|
-
removeListener: vi.fn(),
|
|
177
|
-
kill: vi.fn(),
|
|
178
|
-
};
|
|
179
|
-
setTimeout(() => {
|
|
180
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
181
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
182
|
-
if (onData) {
|
|
183
|
-
// Only return match from the .js file in sub directory
|
|
184
|
-
onData(Buffer.from(`another.js:1:const greeting = "hello";${EOL}`));
|
|
185
|
-
}
|
|
186
|
-
if (onClose) {
|
|
187
|
-
onClose(0);
|
|
188
|
-
}
|
|
189
|
-
}, 0);
|
|
190
|
-
return mockProcess;
|
|
191
|
-
});
|
|
192
|
-
const params = {
|
|
193
|
-
pattern: 'hello',
|
|
194
|
-
path: 'sub',
|
|
195
|
-
include: '*.js',
|
|
196
|
-
};
|
|
197
|
-
const invocation = grepTool.build(params);
|
|
198
|
-
const result = await invocation.execute(abortSignal);
|
|
199
|
-
expect(result.llmContent).toContain('Found 1 match for pattern "hello" in path "sub" (filter: "*.js")');
|
|
200
|
-
expect(result.llmContent).toContain('File: another.js');
|
|
201
|
-
expect(result.llmContent).toContain('L1: const greeting = "hello";');
|
|
202
|
-
expect(result.returnDisplay).toBe('Found 1 match');
|
|
203
|
-
});
|
|
204
|
-
it('should return "No matches found" when pattern does not exist', async () => {
|
|
205
|
-
// Setup specific mock for no matches
|
|
206
|
-
mockSpawn.mockImplementationOnce(createMockSpawn({
|
|
207
|
-
exitCode: 1, // No matches found
|
|
208
|
-
}));
|
|
209
|
-
const params = { pattern: 'nonexistentpattern' };
|
|
210
|
-
const invocation = grepTool.build(params);
|
|
211
|
-
const result = await invocation.execute(abortSignal);
|
|
212
|
-
expect(result.llmContent).toContain('No matches found for pattern "nonexistentpattern" in the workspace directory.');
|
|
213
|
-
expect(result.returnDisplay).toBe('No matches found');
|
|
214
|
-
});
|
|
215
|
-
it('should return an error from ripgrep for invalid regex pattern', async () => {
|
|
216
|
-
mockSpawn.mockImplementationOnce(createMockSpawn({
|
|
217
|
-
exitCode: 2,
|
|
218
|
-
}));
|
|
219
|
-
const params = { pattern: '[[' };
|
|
220
|
-
const invocation = grepTool.build(params);
|
|
221
|
-
const result = await invocation.execute(abortSignal);
|
|
222
|
-
expect(result.llmContent).toContain('ripgrep exited with code 2');
|
|
223
|
-
expect(result.returnDisplay).toContain('Error: ripgrep exited with code 2');
|
|
224
|
-
});
|
|
225
|
-
it('should handle regex special characters correctly', async () => {
|
|
226
|
-
// Setup specific mock for this test - regex pattern 'foo.*bar' should match 'const foo = "bar";'
|
|
227
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
228
|
-
const mockProcess = {
|
|
229
|
-
stdout: {
|
|
230
|
-
on: vi.fn(),
|
|
231
|
-
removeListener: vi.fn(),
|
|
232
|
-
},
|
|
233
|
-
stderr: {
|
|
234
|
-
on: vi.fn(),
|
|
235
|
-
removeListener: vi.fn(),
|
|
236
|
-
},
|
|
237
|
-
on: vi.fn(),
|
|
238
|
-
removeListener: vi.fn(),
|
|
239
|
-
kill: vi.fn(),
|
|
240
|
-
};
|
|
241
|
-
setTimeout(() => {
|
|
242
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
243
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
244
|
-
if (onData) {
|
|
245
|
-
// Return match for the regex pattern
|
|
246
|
-
onData(Buffer.from(`fileB.js:1:const foo = "bar";${EOL}`));
|
|
247
|
-
}
|
|
248
|
-
if (onClose) {
|
|
249
|
-
onClose(0);
|
|
250
|
-
}
|
|
251
|
-
}, 0);
|
|
252
|
-
return mockProcess;
|
|
253
|
-
});
|
|
254
|
-
const params = { pattern: 'foo.*bar' }; // Matches 'const foo = "bar";'
|
|
255
|
-
const invocation = grepTool.build(params);
|
|
256
|
-
const result = await invocation.execute(abortSignal);
|
|
257
|
-
expect(result.llmContent).toContain('Found 1 match for pattern "foo.*bar" in the workspace directory:');
|
|
258
|
-
expect(result.llmContent).toContain('File: fileB.js');
|
|
259
|
-
expect(result.llmContent).toContain('L1: const foo = "bar";');
|
|
260
|
-
});
|
|
261
|
-
it('should be case-insensitive by default (JS fallback)', async () => {
|
|
262
|
-
// Setup specific mock for this test - case insensitive search for 'HELLO'
|
|
263
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
264
|
-
const mockProcess = {
|
|
265
|
-
stdout: {
|
|
266
|
-
on: vi.fn(),
|
|
267
|
-
removeListener: vi.fn(),
|
|
268
|
-
},
|
|
269
|
-
stderr: {
|
|
270
|
-
on: vi.fn(),
|
|
271
|
-
removeListener: vi.fn(),
|
|
272
|
-
},
|
|
273
|
-
on: vi.fn(),
|
|
274
|
-
removeListener: vi.fn(),
|
|
275
|
-
kill: vi.fn(),
|
|
276
|
-
};
|
|
277
|
-
setTimeout(() => {
|
|
278
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
279
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
280
|
-
if (onData) {
|
|
281
|
-
// Return case-insensitive matches for 'HELLO'
|
|
282
|
-
onData(Buffer.from(`fileA.txt:1:hello world${EOL}fileB.js:2:function baz() { return "hello"; }${EOL}`));
|
|
283
|
-
}
|
|
284
|
-
if (onClose) {
|
|
285
|
-
onClose(0);
|
|
286
|
-
}
|
|
287
|
-
}, 0);
|
|
288
|
-
return mockProcess;
|
|
289
|
-
});
|
|
290
|
-
const params = { pattern: 'HELLO' };
|
|
291
|
-
const invocation = grepTool.build(params);
|
|
292
|
-
const result = await invocation.execute(abortSignal);
|
|
293
|
-
expect(result.llmContent).toContain('Found 2 matches for pattern "HELLO" in the workspace directory:');
|
|
294
|
-
expect(result.llmContent).toContain('File: fileA.txt');
|
|
295
|
-
expect(result.llmContent).toContain('L1: hello world');
|
|
296
|
-
expect(result.llmContent).toContain('File: fileB.js');
|
|
297
|
-
expect(result.llmContent).toContain('L2: function baz() { return "hello"; }');
|
|
298
|
-
});
|
|
299
|
-
it('should throw an error if params are invalid', async () => {
|
|
300
|
-
const params = { path: '.' }; // Invalid: pattern missing
|
|
301
|
-
expect(() => grepTool.build(params)).toThrow(/params must have required property 'pattern'/);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
describe('multi-directory workspace', () => {
|
|
305
|
-
it('should search across all workspace directories when no path is specified', async () => {
|
|
306
|
-
// Create additional directory with test files
|
|
307
|
-
const secondDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-second-'));
|
|
308
|
-
await fs.writeFile(path.join(secondDir, 'other.txt'), 'hello from second directory\nworld in second');
|
|
309
|
-
await fs.writeFile(path.join(secondDir, 'another.js'), 'function world() { return "test"; }');
|
|
310
|
-
// Create a mock config with multiple directories
|
|
311
|
-
const multiDirConfig = {
|
|
312
|
-
getTargetDir: () => tempRootDir,
|
|
313
|
-
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir, [secondDir]),
|
|
314
|
-
getDebugMode: () => false,
|
|
315
|
-
};
|
|
316
|
-
// Setup specific mock for this test - multi-directory search for 'world'
|
|
317
|
-
// Mock will be called twice - once for each directory
|
|
318
|
-
let callCount = 0;
|
|
319
|
-
mockSpawn.mockImplementation(() => {
|
|
320
|
-
callCount++;
|
|
321
|
-
const mockProcess = {
|
|
322
|
-
stdout: {
|
|
323
|
-
on: vi.fn(),
|
|
324
|
-
removeListener: vi.fn(),
|
|
325
|
-
},
|
|
326
|
-
stderr: {
|
|
327
|
-
on: vi.fn(),
|
|
328
|
-
removeListener: vi.fn(),
|
|
329
|
-
},
|
|
330
|
-
on: vi.fn(),
|
|
331
|
-
removeListener: vi.fn(),
|
|
332
|
-
kill: vi.fn(),
|
|
333
|
-
};
|
|
334
|
-
setTimeout(() => {
|
|
335
|
-
const stdoutDataHandler = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
336
|
-
const closeHandler = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
337
|
-
let outputData = '';
|
|
338
|
-
if (callCount === 1) {
|
|
339
|
-
// First directory (tempRootDir)
|
|
340
|
-
outputData =
|
|
341
|
-
[
|
|
342
|
-
'fileA.txt:1:hello world',
|
|
343
|
-
'fileA.txt:2:second line with world',
|
|
344
|
-
'sub/fileC.txt:1:another world in sub dir',
|
|
345
|
-
].join(EOL) + EOL;
|
|
346
|
-
}
|
|
347
|
-
else if (callCount === 2) {
|
|
348
|
-
// Second directory (secondDir)
|
|
349
|
-
outputData =
|
|
350
|
-
[
|
|
351
|
-
'other.txt:2:world in second',
|
|
352
|
-
'another.js:1:function world() { return "test"; }',
|
|
353
|
-
].join(EOL) + EOL;
|
|
354
|
-
}
|
|
355
|
-
if (stdoutDataHandler && outputData) {
|
|
356
|
-
stdoutDataHandler(Buffer.from(outputData));
|
|
357
|
-
}
|
|
358
|
-
if (closeHandler) {
|
|
359
|
-
closeHandler(0);
|
|
360
|
-
}
|
|
361
|
-
}, 0);
|
|
362
|
-
return mockProcess;
|
|
363
|
-
});
|
|
364
|
-
const multiDirGrepTool = new RipGrepTool(multiDirConfig);
|
|
365
|
-
const params = { pattern: 'world' };
|
|
366
|
-
const invocation = multiDirGrepTool.build(params);
|
|
367
|
-
const result = await invocation.execute(abortSignal);
|
|
368
|
-
// Should find matches in both directories
|
|
369
|
-
expect(result.llmContent).toContain('Found 5 matches for pattern "world"');
|
|
370
|
-
// Matches from first directory
|
|
371
|
-
expect(result.llmContent).toContain('fileA.txt');
|
|
372
|
-
expect(result.llmContent).toContain('L1: hello world');
|
|
373
|
-
expect(result.llmContent).toContain('L2: second line with world');
|
|
374
|
-
expect(result.llmContent).toContain('fileC.txt');
|
|
375
|
-
expect(result.llmContent).toContain('L1: another world in sub dir');
|
|
376
|
-
// Matches from both directories
|
|
377
|
-
expect(result.llmContent).toContain('other.txt');
|
|
378
|
-
expect(result.llmContent).toContain('L2: world in second');
|
|
379
|
-
expect(result.llmContent).toContain('another.js');
|
|
380
|
-
expect(result.llmContent).toContain('L1: function world()');
|
|
381
|
-
// Clean up
|
|
382
|
-
await fs.rm(secondDir, { recursive: true, force: true });
|
|
383
|
-
mockSpawn.mockClear();
|
|
384
|
-
});
|
|
385
|
-
it('should search only specified path within workspace directories', async () => {
|
|
386
|
-
// Create additional directory
|
|
387
|
-
const secondDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-second-'));
|
|
388
|
-
await fs.mkdir(path.join(secondDir, 'sub'));
|
|
389
|
-
await fs.writeFile(path.join(secondDir, 'sub', 'test.txt'), 'hello from second sub directory');
|
|
390
|
-
// Create a mock config with multiple directories
|
|
391
|
-
const multiDirConfig = {
|
|
392
|
-
getTargetDir: () => tempRootDir,
|
|
393
|
-
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir, [secondDir]),
|
|
394
|
-
getDebugMode: () => false,
|
|
395
|
-
};
|
|
396
|
-
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
|
397
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
398
|
-
const mockProcess = {
|
|
399
|
-
stdout: {
|
|
400
|
-
on: vi.fn(),
|
|
401
|
-
removeListener: vi.fn(),
|
|
402
|
-
},
|
|
403
|
-
stderr: {
|
|
404
|
-
on: vi.fn(),
|
|
405
|
-
removeListener: vi.fn(),
|
|
406
|
-
},
|
|
407
|
-
on: vi.fn(),
|
|
408
|
-
removeListener: vi.fn(),
|
|
409
|
-
kill: vi.fn(),
|
|
410
|
-
};
|
|
411
|
-
setTimeout(() => {
|
|
412
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
413
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
414
|
-
if (onData) {
|
|
415
|
-
onData(Buffer.from(`fileC.txt:1:another world in sub dir${EOL}`));
|
|
416
|
-
}
|
|
417
|
-
if (onClose) {
|
|
418
|
-
onClose(0);
|
|
419
|
-
}
|
|
420
|
-
}, 0);
|
|
421
|
-
return mockProcess;
|
|
422
|
-
});
|
|
423
|
-
const multiDirGrepTool = new RipGrepTool(multiDirConfig);
|
|
424
|
-
// Search only in the 'sub' directory of the first workspace
|
|
425
|
-
const params = { pattern: 'world', path: 'sub' };
|
|
426
|
-
const invocation = multiDirGrepTool.build(params);
|
|
427
|
-
const result = await invocation.execute(abortSignal);
|
|
428
|
-
// Should only find matches in the specified sub directory
|
|
429
|
-
expect(result.llmContent).toContain('Found 1 match for pattern "world" in path "sub"');
|
|
430
|
-
expect(result.llmContent).toContain('File: fileC.txt');
|
|
431
|
-
expect(result.llmContent).toContain('L1: another world in sub dir');
|
|
432
|
-
// Should not contain matches from second directory
|
|
433
|
-
expect(result.llmContent).not.toContain('test.txt');
|
|
434
|
-
// Clean up
|
|
435
|
-
await fs.rm(secondDir, { recursive: true, force: true });
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
describe('abort signal handling', () => {
|
|
439
|
-
it('should handle AbortSignal during search', async () => {
|
|
440
|
-
const controller = new AbortController();
|
|
441
|
-
const params = { pattern: 'world' };
|
|
442
|
-
const invocation = grepTool.build(params);
|
|
443
|
-
controller.abort();
|
|
444
|
-
const result = await invocation.execute(controller.signal);
|
|
445
|
-
expect(result).toBeDefined();
|
|
446
|
-
});
|
|
447
|
-
it('should abort streaming search when signal is triggered', async () => {
|
|
448
|
-
// Setup specific mock for this test - simulate process being killed due to abort
|
|
449
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
450
|
-
const mockProcess = {
|
|
451
|
-
stdout: {
|
|
452
|
-
on: vi.fn(),
|
|
453
|
-
removeListener: vi.fn(),
|
|
454
|
-
},
|
|
455
|
-
stderr: {
|
|
456
|
-
on: vi.fn(),
|
|
457
|
-
removeListener: vi.fn(),
|
|
458
|
-
},
|
|
459
|
-
on: vi.fn(),
|
|
460
|
-
removeListener: vi.fn(),
|
|
461
|
-
kill: vi.fn(),
|
|
462
|
-
};
|
|
463
|
-
// Simulate process being aborted - use setTimeout to ensure handlers are registered first
|
|
464
|
-
setTimeout(() => {
|
|
465
|
-
const closeHandler = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
466
|
-
if (closeHandler) {
|
|
467
|
-
// Simulate process killed by signal (code is null, signal is SIGTERM)
|
|
468
|
-
closeHandler(null, 'SIGTERM');
|
|
469
|
-
}
|
|
470
|
-
}, 0);
|
|
471
|
-
return mockProcess;
|
|
472
|
-
});
|
|
473
|
-
const controller = new AbortController();
|
|
474
|
-
const params = { pattern: 'test' };
|
|
475
|
-
const invocation = grepTool.build(params);
|
|
476
|
-
// Abort immediately before starting the search
|
|
477
|
-
controller.abort();
|
|
478
|
-
const result = await invocation.execute(controller.signal);
|
|
479
|
-
expect(result.llmContent).toContain('Error during grep search operation: ripgrep exited with code null');
|
|
480
|
-
expect(result.returnDisplay).toContain('Error: ripgrep exited with code null');
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
describe('error handling and edge cases', () => {
|
|
484
|
-
it('should handle workspace boundary violations', () => {
|
|
485
|
-
const params = { pattern: 'test', path: '../outside' };
|
|
486
|
-
expect(() => grepTool.build(params)).toThrow(/Path validation failed/);
|
|
487
|
-
});
|
|
488
|
-
it('should handle empty directories gracefully', async () => {
|
|
489
|
-
const emptyDir = path.join(tempRootDir, 'empty');
|
|
490
|
-
await fs.mkdir(emptyDir);
|
|
491
|
-
// Setup specific mock for this test - searching in empty directory should return no matches
|
|
492
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
493
|
-
const mockProcess = {
|
|
494
|
-
stdout: {
|
|
495
|
-
on: vi.fn(),
|
|
496
|
-
removeListener: vi.fn(),
|
|
497
|
-
},
|
|
498
|
-
stderr: {
|
|
499
|
-
on: vi.fn(),
|
|
500
|
-
removeListener: vi.fn(),
|
|
501
|
-
},
|
|
502
|
-
on: vi.fn(),
|
|
503
|
-
removeListener: vi.fn(),
|
|
504
|
-
kill: vi.fn(),
|
|
505
|
-
};
|
|
506
|
-
setTimeout(() => {
|
|
507
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
508
|
-
if (onClose) {
|
|
509
|
-
onClose(1);
|
|
510
|
-
}
|
|
511
|
-
}, 0);
|
|
512
|
-
return mockProcess;
|
|
513
|
-
});
|
|
514
|
-
const params = { pattern: 'test', path: 'empty' };
|
|
515
|
-
const invocation = grepTool.build(params);
|
|
516
|
-
const result = await invocation.execute(abortSignal);
|
|
517
|
-
expect(result.llmContent).toContain('No matches found');
|
|
518
|
-
expect(result.returnDisplay).toBe('No matches found');
|
|
519
|
-
});
|
|
520
|
-
it('should handle empty files correctly', async () => {
|
|
521
|
-
await fs.writeFile(path.join(tempRootDir, 'empty.txt'), '');
|
|
522
|
-
// Setup specific mock for this test - searching for anything in empty files should return no matches
|
|
523
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
524
|
-
const mockProcess = {
|
|
525
|
-
stdout: {
|
|
526
|
-
on: vi.fn(),
|
|
527
|
-
removeListener: vi.fn(),
|
|
528
|
-
},
|
|
529
|
-
stderr: {
|
|
530
|
-
on: vi.fn(),
|
|
531
|
-
removeListener: vi.fn(),
|
|
532
|
-
},
|
|
533
|
-
on: vi.fn(),
|
|
534
|
-
removeListener: vi.fn(),
|
|
535
|
-
kill: vi.fn(),
|
|
536
|
-
};
|
|
537
|
-
setTimeout(() => {
|
|
538
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
539
|
-
if (onClose) {
|
|
540
|
-
onClose(1);
|
|
541
|
-
}
|
|
542
|
-
}, 0);
|
|
543
|
-
return mockProcess;
|
|
544
|
-
});
|
|
545
|
-
const params = { pattern: 'anything' };
|
|
546
|
-
const invocation = grepTool.build(params);
|
|
547
|
-
const result = await invocation.execute(abortSignal);
|
|
548
|
-
expect(result.llmContent).toContain('No matches found');
|
|
549
|
-
});
|
|
550
|
-
it('should handle special characters in file names', async () => {
|
|
551
|
-
const specialFileName = 'file with spaces & symbols!.txt';
|
|
552
|
-
await fs.writeFile(path.join(tempRootDir, specialFileName), 'hello world with special chars');
|
|
553
|
-
// Setup specific mock for this test - searching for 'world' should find the file with special characters
|
|
554
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
555
|
-
const mockProcess = {
|
|
556
|
-
stdout: {
|
|
557
|
-
on: vi.fn(),
|
|
558
|
-
removeListener: vi.fn(),
|
|
559
|
-
},
|
|
560
|
-
stderr: {
|
|
561
|
-
on: vi.fn(),
|
|
562
|
-
removeListener: vi.fn(),
|
|
563
|
-
},
|
|
564
|
-
on: vi.fn(),
|
|
565
|
-
removeListener: vi.fn(),
|
|
566
|
-
kill: vi.fn(),
|
|
567
|
-
};
|
|
568
|
-
setTimeout(() => {
|
|
569
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
570
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
571
|
-
if (onData) {
|
|
572
|
-
onData(Buffer.from(`${specialFileName}:1:hello world with special chars${EOL}`));
|
|
573
|
-
}
|
|
574
|
-
if (onClose) {
|
|
575
|
-
onClose(0);
|
|
576
|
-
}
|
|
577
|
-
}, 0);
|
|
578
|
-
return mockProcess;
|
|
579
|
-
});
|
|
580
|
-
const params = { pattern: 'world' };
|
|
581
|
-
const invocation = grepTool.build(params);
|
|
582
|
-
const result = await invocation.execute(abortSignal);
|
|
583
|
-
expect(result.llmContent).toContain(specialFileName);
|
|
584
|
-
expect(result.llmContent).toContain('hello world with special chars');
|
|
585
|
-
});
|
|
586
|
-
it('should handle deeply nested directories', async () => {
|
|
587
|
-
const deepPath = path.join(tempRootDir, 'a', 'b', 'c', 'd', 'e');
|
|
588
|
-
await fs.mkdir(deepPath, { recursive: true });
|
|
589
|
-
await fs.writeFile(path.join(deepPath, 'deep.txt'), 'content in deep directory');
|
|
590
|
-
// Setup specific mock for this test - searching for 'deep' should find the deeply nested file
|
|
591
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
592
|
-
const mockProcess = {
|
|
593
|
-
stdout: {
|
|
594
|
-
on: vi.fn(),
|
|
595
|
-
removeListener: vi.fn(),
|
|
596
|
-
},
|
|
597
|
-
stderr: {
|
|
598
|
-
on: vi.fn(),
|
|
599
|
-
removeListener: vi.fn(),
|
|
600
|
-
},
|
|
601
|
-
on: vi.fn(),
|
|
602
|
-
removeListener: vi.fn(),
|
|
603
|
-
kill: vi.fn(),
|
|
604
|
-
};
|
|
605
|
-
setTimeout(() => {
|
|
606
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
607
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
608
|
-
if (onData) {
|
|
609
|
-
onData(Buffer.from(`a/b/c/d/e/deep.txt:1:content in deep directory${EOL}`));
|
|
610
|
-
}
|
|
611
|
-
if (onClose) {
|
|
612
|
-
onClose(0);
|
|
613
|
-
}
|
|
614
|
-
}, 0);
|
|
615
|
-
return mockProcess;
|
|
616
|
-
});
|
|
617
|
-
const params = { pattern: 'deep' };
|
|
618
|
-
const invocation = grepTool.build(params);
|
|
619
|
-
const result = await invocation.execute(abortSignal);
|
|
620
|
-
expect(result.llmContent).toContain('deep.txt');
|
|
621
|
-
expect(result.llmContent).toContain('content in deep directory');
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
describe('regex pattern validation', () => {
|
|
625
|
-
it('should handle complex regex patterns', async () => {
|
|
626
|
-
await fs.writeFile(path.join(tempRootDir, 'code.js'), 'function getName() { return "test"; }\nconst getValue = () => "value";');
|
|
627
|
-
// Setup specific mock for this test - regex pattern should match function declarations
|
|
628
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
629
|
-
const mockProcess = {
|
|
630
|
-
stdout: {
|
|
631
|
-
on: vi.fn(),
|
|
632
|
-
removeListener: vi.fn(),
|
|
633
|
-
},
|
|
634
|
-
stderr: {
|
|
635
|
-
on: vi.fn(),
|
|
636
|
-
removeListener: vi.fn(),
|
|
637
|
-
},
|
|
638
|
-
on: vi.fn(),
|
|
639
|
-
removeListener: vi.fn(),
|
|
640
|
-
kill: vi.fn(),
|
|
641
|
-
};
|
|
642
|
-
setTimeout(() => {
|
|
643
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
644
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
645
|
-
if (onData) {
|
|
646
|
-
onData(Buffer.from(`code.js:1:function getName() { return "test"; }${EOL}`));
|
|
647
|
-
}
|
|
648
|
-
if (onClose) {
|
|
649
|
-
onClose(0);
|
|
650
|
-
}
|
|
651
|
-
}, 0);
|
|
652
|
-
return mockProcess;
|
|
653
|
-
});
|
|
654
|
-
const params = { pattern: 'function\\s+\\w+\\s*\\(' };
|
|
655
|
-
const invocation = grepTool.build(params);
|
|
656
|
-
const result = await invocation.execute(abortSignal);
|
|
657
|
-
expect(result.llmContent).toContain('function getName()');
|
|
658
|
-
expect(result.llmContent).not.toContain('const getValue');
|
|
659
|
-
});
|
|
660
|
-
it('should handle case sensitivity correctly in JS fallback', async () => {
|
|
661
|
-
await fs.writeFile(path.join(tempRootDir, 'case.txt'), 'Hello World\nhello world\nHELLO WORLD');
|
|
662
|
-
// Setup specific mock for this test - case insensitive search should match all variants
|
|
663
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
664
|
-
const mockProcess = {
|
|
665
|
-
stdout: {
|
|
666
|
-
on: vi.fn(),
|
|
667
|
-
removeListener: vi.fn(),
|
|
668
|
-
},
|
|
669
|
-
stderr: {
|
|
670
|
-
on: vi.fn(),
|
|
671
|
-
removeListener: vi.fn(),
|
|
672
|
-
},
|
|
673
|
-
on: vi.fn(),
|
|
674
|
-
removeListener: vi.fn(),
|
|
675
|
-
kill: vi.fn(),
|
|
676
|
-
};
|
|
677
|
-
setTimeout(() => {
|
|
678
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
679
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
680
|
-
if (onData) {
|
|
681
|
-
onData(Buffer.from(`case.txt:1:Hello World${EOL}case.txt:2:hello world${EOL}case.txt:3:HELLO WORLD${EOL}`));
|
|
682
|
-
}
|
|
683
|
-
if (onClose) {
|
|
684
|
-
onClose(0);
|
|
685
|
-
}
|
|
686
|
-
}, 0);
|
|
687
|
-
return mockProcess;
|
|
688
|
-
});
|
|
689
|
-
const params = { pattern: 'hello' };
|
|
690
|
-
const invocation = grepTool.build(params);
|
|
691
|
-
const result = await invocation.execute(abortSignal);
|
|
692
|
-
expect(result.llmContent).toContain('Hello World');
|
|
693
|
-
expect(result.llmContent).toContain('hello world');
|
|
694
|
-
expect(result.llmContent).toContain('HELLO WORLD');
|
|
695
|
-
});
|
|
696
|
-
it('should handle escaped regex special characters', async () => {
|
|
697
|
-
await fs.writeFile(path.join(tempRootDir, 'special.txt'), 'Price: $19.99\nRegex: [a-z]+ pattern\nEmail: test@example.com');
|
|
698
|
-
// Setup specific mock for this test - escaped regex pattern should match price format
|
|
699
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
700
|
-
const mockProcess = {
|
|
701
|
-
stdout: {
|
|
702
|
-
on: vi.fn(),
|
|
703
|
-
removeListener: vi.fn(),
|
|
704
|
-
},
|
|
705
|
-
stderr: {
|
|
706
|
-
on: vi.fn(),
|
|
707
|
-
removeListener: vi.fn(),
|
|
708
|
-
},
|
|
709
|
-
on: vi.fn(),
|
|
710
|
-
removeListener: vi.fn(),
|
|
711
|
-
kill: vi.fn(),
|
|
712
|
-
};
|
|
713
|
-
setTimeout(() => {
|
|
714
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
715
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
716
|
-
if (onData) {
|
|
717
|
-
onData(Buffer.from(`special.txt:1:Price: $19.99${EOL}`));
|
|
718
|
-
}
|
|
719
|
-
if (onClose) {
|
|
720
|
-
onClose(0);
|
|
721
|
-
}
|
|
722
|
-
}, 0);
|
|
723
|
-
return mockProcess;
|
|
724
|
-
});
|
|
725
|
-
const params = { pattern: '\\$\\d+\\.\\d+' };
|
|
726
|
-
const invocation = grepTool.build(params);
|
|
727
|
-
const result = await invocation.execute(abortSignal);
|
|
728
|
-
expect(result.llmContent).toContain('Price: $19.99');
|
|
729
|
-
expect(result.llmContent).not.toContain('Email: test@example.com');
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
describe('include pattern filtering', () => {
|
|
733
|
-
it('should handle multiple file extensions in include pattern', async () => {
|
|
734
|
-
await fs.writeFile(path.join(tempRootDir, 'test.ts'), 'typescript content');
|
|
735
|
-
await fs.writeFile(path.join(tempRootDir, 'test.tsx'), 'tsx content');
|
|
736
|
-
await fs.writeFile(path.join(tempRootDir, 'test.js'), 'javascript content');
|
|
737
|
-
await fs.writeFile(path.join(tempRootDir, 'test.txt'), 'text content');
|
|
738
|
-
// Setup specific mock for this test - include pattern should filter to only ts/tsx files
|
|
739
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
740
|
-
const mockProcess = {
|
|
741
|
-
stdout: {
|
|
742
|
-
on: vi.fn(),
|
|
743
|
-
removeListener: vi.fn(),
|
|
744
|
-
},
|
|
745
|
-
stderr: {
|
|
746
|
-
on: vi.fn(),
|
|
747
|
-
removeListener: vi.fn(),
|
|
748
|
-
},
|
|
749
|
-
on: vi.fn(),
|
|
750
|
-
removeListener: vi.fn(),
|
|
751
|
-
kill: vi.fn(),
|
|
752
|
-
};
|
|
753
|
-
setTimeout(() => {
|
|
754
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
755
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
756
|
-
if (onData) {
|
|
757
|
-
onData(Buffer.from(`test.ts:1:typescript content${EOL}test.tsx:1:tsx content${EOL}`));
|
|
758
|
-
}
|
|
759
|
-
if (onClose) {
|
|
760
|
-
onClose(0);
|
|
761
|
-
}
|
|
762
|
-
}, 0);
|
|
763
|
-
return mockProcess;
|
|
764
|
-
});
|
|
765
|
-
const params = {
|
|
766
|
-
pattern: 'content',
|
|
767
|
-
include: '*.{ts,tsx}',
|
|
768
|
-
};
|
|
769
|
-
const invocation = grepTool.build(params);
|
|
770
|
-
const result = await invocation.execute(abortSignal);
|
|
771
|
-
expect(result.llmContent).toContain('test.ts');
|
|
772
|
-
expect(result.llmContent).toContain('test.tsx');
|
|
773
|
-
expect(result.llmContent).not.toContain('test.js');
|
|
774
|
-
expect(result.llmContent).not.toContain('test.txt');
|
|
775
|
-
});
|
|
776
|
-
it('should handle directory patterns in include', async () => {
|
|
777
|
-
await fs.mkdir(path.join(tempRootDir, 'src'), { recursive: true });
|
|
778
|
-
await fs.writeFile(path.join(tempRootDir, 'src', 'main.ts'), 'source code');
|
|
779
|
-
await fs.writeFile(path.join(tempRootDir, 'other.ts'), 'other code');
|
|
780
|
-
// Setup specific mock for this test - include pattern should filter to only src/** files
|
|
781
|
-
mockSpawn.mockImplementationOnce(() => {
|
|
782
|
-
const mockProcess = {
|
|
783
|
-
stdout: {
|
|
784
|
-
on: vi.fn(),
|
|
785
|
-
removeListener: vi.fn(),
|
|
786
|
-
},
|
|
787
|
-
stderr: {
|
|
788
|
-
on: vi.fn(),
|
|
789
|
-
removeListener: vi.fn(),
|
|
790
|
-
},
|
|
791
|
-
on: vi.fn(),
|
|
792
|
-
removeListener: vi.fn(),
|
|
793
|
-
kill: vi.fn(),
|
|
794
|
-
};
|
|
795
|
-
setTimeout(() => {
|
|
796
|
-
const onData = mockProcess.stdout.on.mock.calls.find((call) => call[0] === 'data')?.[1];
|
|
797
|
-
const onClose = mockProcess.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
798
|
-
if (onData) {
|
|
799
|
-
onData(Buffer.from(`src/main.ts:1:source code${EOL}`));
|
|
800
|
-
}
|
|
801
|
-
if (onClose) {
|
|
802
|
-
onClose(0);
|
|
803
|
-
}
|
|
804
|
-
}, 0);
|
|
805
|
-
return mockProcess;
|
|
806
|
-
});
|
|
807
|
-
const params = {
|
|
808
|
-
pattern: 'code',
|
|
809
|
-
include: 'src/**',
|
|
810
|
-
};
|
|
811
|
-
const invocation = grepTool.build(params);
|
|
812
|
-
const result = await invocation.execute(abortSignal);
|
|
813
|
-
expect(result.llmContent).toContain('main.ts');
|
|
814
|
-
expect(result.llmContent).not.toContain('other.ts');
|
|
815
|
-
});
|
|
816
|
-
});
|
|
817
|
-
describe('getDescription', () => {
|
|
818
|
-
it('should generate correct description with pattern only', () => {
|
|
819
|
-
const params = { pattern: 'testPattern' };
|
|
820
|
-
const invocation = grepTool.build(params);
|
|
821
|
-
expect(invocation.getDescription()).toBe("'testPattern'");
|
|
822
|
-
});
|
|
823
|
-
it('should generate correct description with pattern and include', () => {
|
|
824
|
-
const params = {
|
|
825
|
-
pattern: 'testPattern',
|
|
826
|
-
include: '*.ts',
|
|
827
|
-
};
|
|
828
|
-
const invocation = grepTool.build(params);
|
|
829
|
-
expect(invocation.getDescription()).toBe("'testPattern' in *.ts");
|
|
830
|
-
});
|
|
831
|
-
it('should generate correct description with pattern and path', async () => {
|
|
832
|
-
const dirPath = path.join(tempRootDir, 'src', 'app');
|
|
833
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
834
|
-
const params = {
|
|
835
|
-
pattern: 'testPattern',
|
|
836
|
-
path: path.join('src', 'app'),
|
|
837
|
-
};
|
|
838
|
-
const invocation = grepTool.build(params);
|
|
839
|
-
// The path will be relative to the tempRootDir, so we check for containment.
|
|
840
|
-
expect(invocation.getDescription()).toContain("'testPattern' within");
|
|
841
|
-
expect(invocation.getDescription()).toContain(path.join('src', 'app'));
|
|
842
|
-
});
|
|
843
|
-
it('should indicate searching across all workspace directories when no path specified', () => {
|
|
844
|
-
// Create a mock config with multiple directories
|
|
845
|
-
const multiDirConfig = {
|
|
846
|
-
getTargetDir: () => tempRootDir,
|
|
847
|
-
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir, ['/another/dir']),
|
|
848
|
-
getDebugMode: () => false,
|
|
849
|
-
};
|
|
850
|
-
const multiDirGrepTool = new RipGrepTool(multiDirConfig);
|
|
851
|
-
const params = { pattern: 'testPattern' };
|
|
852
|
-
const invocation = multiDirGrepTool.build(params);
|
|
853
|
-
expect(invocation.getDescription()).toBe("'testPattern' across all workspace directories");
|
|
854
|
-
});
|
|
855
|
-
it('should generate correct description with pattern, include, and path', async () => {
|
|
856
|
-
const dirPath = path.join(tempRootDir, 'src', 'app');
|
|
857
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
858
|
-
const params = {
|
|
859
|
-
pattern: 'testPattern',
|
|
860
|
-
include: '*.ts',
|
|
861
|
-
path: path.join('src', 'app'),
|
|
862
|
-
};
|
|
863
|
-
const invocation = grepTool.build(params);
|
|
864
|
-
expect(invocation.getDescription()).toContain("'testPattern' in *.ts within");
|
|
865
|
-
expect(invocation.getDescription()).toContain(path.join('src', 'app'));
|
|
866
|
-
});
|
|
867
|
-
it('should use ./ for root path in description', () => {
|
|
868
|
-
const params = { pattern: 'testPattern', path: '.' };
|
|
869
|
-
const invocation = grepTool.build(params);
|
|
870
|
-
expect(invocation.getDescription()).toBe("'testPattern' within ./");
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
//# sourceMappingURL=ripGrep.test.js.map
|