onbuzz 3.6.1 → 3.6.2

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.
Files changed (83) hide show
  1. package/package.json +1 -1
  2. package/src/__test-utils__/fixtures/malformedJson.js +31 -0
  3. package/src/__test-utils__/globalSetup.js +9 -0
  4. package/src/__test-utils__/globalTeardown.js +12 -0
  5. package/src/__test-utils__/mockFactories.js +101 -0
  6. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
  7. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
  8. package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
  9. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
  10. package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
  11. package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
  12. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
  13. package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
  14. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
  15. package/src/core/__tests__/agentPool.test.js +601 -0
  16. package/src/core/__tests__/agentScheduler.test.js +576 -0
  17. package/src/core/__tests__/contextManager.test.js +252 -0
  18. package/src/core/__tests__/flowExecutor.test.js +262 -0
  19. package/src/core/__tests__/messageProcessor.test.js +627 -0
  20. package/src/core/__tests__/orchestrator.test.js +257 -0
  21. package/src/core/__tests__/stateManager.test.js +375 -0
  22. package/src/core/agentPool.js +11 -1
  23. package/src/index.js +25 -9
  24. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
  25. package/src/services/__tests__/agentActivityService.test.js +319 -0
  26. package/src/services/__tests__/apiKeyManager.test.js +206 -0
  27. package/src/services/__tests__/benchmarkService.test.js +184 -0
  28. package/src/services/__tests__/budgetService.test.js +211 -0
  29. package/src/services/__tests__/contextInjectionService.test.js +205 -0
  30. package/src/services/__tests__/conversationCompactionService.test.js +280 -0
  31. package/src/services/__tests__/credentialVault.test.js +469 -0
  32. package/src/services/__tests__/errorHandler.test.js +314 -0
  33. package/src/services/__tests__/fileAttachmentService.test.js +278 -0
  34. package/src/services/__tests__/flowContextService.test.js +199 -0
  35. package/src/services/__tests__/memoryService.test.js +450 -0
  36. package/src/services/__tests__/modelRouterService.test.js +388 -0
  37. package/src/services/__tests__/modelsService.test.js +261 -0
  38. package/src/services/__tests__/portRegistry.test.js +123 -0
  39. package/src/services/__tests__/projectDetector.test.js +34 -0
  40. package/src/services/__tests__/promptService.test.js +242 -0
  41. package/src/services/__tests__/qualityInspector.test.js +97 -0
  42. package/src/services/__tests__/scheduleService.test.js +308 -0
  43. package/src/services/__tests__/serviceRegistry.test.js +74 -0
  44. package/src/services/__tests__/skillsService.test.js +402 -0
  45. package/src/services/__tests__/tokenCountingService.test.js +48 -0
  46. package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
  47. package/src/tools/__tests__/agentDelayTool.test.js +342 -0
  48. package/src/tools/__tests__/asyncToolManager.test.js +344 -0
  49. package/src/tools/__tests__/baseTool.test.js +420 -0
  50. package/src/tools/__tests__/codeMapTool.test.js +348 -0
  51. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
  52. package/src/tools/__tests__/fileTreeTool.test.js +274 -0
  53. package/src/tools/__tests__/filesystemTool.test.js +717 -0
  54. package/src/tools/__tests__/helpTool.test.js +204 -0
  55. package/src/tools/__tests__/jobDoneTool.test.js +296 -0
  56. package/src/tools/__tests__/memoryTool.test.js +297 -0
  57. package/src/tools/__tests__/seekTool.test.js +282 -0
  58. package/src/tools/__tests__/skillsTool.test.js +226 -0
  59. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
  60. package/src/tools/__tests__/taskManagerTool.test.js +725 -0
  61. package/src/tools/__tests__/terminalTool.test.js +384 -0
  62. package/src/tools/__tests__/userPromptTool.test.js +297 -0
  63. package/src/tools/__tests__/webTool.e2e.test.js +25 -11
  64. package/src/tools/webTool.js +6 -12
  65. package/src/types/__tests__/agent.test.js +499 -0
  66. package/src/types/__tests__/contextReference.test.js +606 -0
  67. package/src/types/__tests__/conversation.test.js +555 -0
  68. package/src/types/__tests__/toolCommand.test.js +584 -0
  69. package/src/types/contextReference.js +1 -1
  70. package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
  71. package/src/utilities/__tests__/configManager.test.js +397 -0
  72. package/src/utilities/__tests__/constants.test.js +49 -0
  73. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
  74. package/src/utilities/__tests__/fileProcessor.test.js +104 -0
  75. package/src/utilities/__tests__/jsonRepair.test.js +104 -0
  76. package/src/utilities/__tests__/logger.test.js +129 -0
  77. package/src/utilities/__tests__/platformUtils.test.js +87 -0
  78. package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
  79. package/src/utilities/__tests__/tagParser.test.js +887 -0
  80. package/src/utilities/__tests__/toolConstants.test.js +94 -0
  81. package/src/utilities/tagParser.js +2 -2
  82. package/src/tools/browserTool.js +0 -897
  83. package/src/utilities/platformUtils.test.js +0 -98
@@ -0,0 +1,297 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock constants
5
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
6
+ TOOL_STATUS: { PENDING: 'pending', EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed' },
7
+ OPERATION_STATUS: { NOT_FOUND: 'not_found' },
8
+ ERROR_TYPES: {},
9
+ SYSTEM_DEFAULTS: { MAX_TOOL_EXECUTION_TIME: 300000 }
10
+ }));
11
+
12
+ // Create mock memory service
13
+ const mockMemoryService = {
14
+ initialize: jest.fn().mockResolvedValue(undefined),
15
+ addMemory: jest.fn(),
16
+ updateMemory: jest.fn(),
17
+ deleteMemory: jest.fn(),
18
+ listMemories: jest.fn(),
19
+ readMemory: jest.fn(),
20
+ searchMemories: jest.fn(),
21
+ getMemoryStats: jest.fn()
22
+ };
23
+
24
+ jest.unstable_mockModule('../../services/memoryService.js', () => ({
25
+ getMemoryService: jest.fn(() => mockMemoryService)
26
+ }));
27
+
28
+ const { default: MemoryTool } = await import('../memoryTool.js');
29
+
30
+ describe('MemoryTool', () => {
31
+ let tool;
32
+ let logger;
33
+ const context = { agentId: 'agent-1' };
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ logger = createMockLogger();
38
+ tool = new MemoryTool({}, logger);
39
+ });
40
+
41
+ test('constructor sets metadata correctly', () => {
42
+ expect(tool.id).toBe('memory');
43
+ expect(tool.requiresProject).toBe(false);
44
+ expect(tool.timeout).toBe(30000);
45
+ });
46
+
47
+ test('getDescription contains all actions', () => {
48
+ const desc = tool.getDescription();
49
+ expect(desc).toContain('Memory Tool');
50
+ expect(desc).toContain('add');
51
+ expect(desc).toContain('update');
52
+ expect(desc).toContain('delete');
53
+ expect(desc).toContain('list');
54
+ expect(desc).toContain('read');
55
+ expect(desc).toContain('search');
56
+ expect(desc).toContain('stats');
57
+ });
58
+
59
+ test('getSupportedActions returns all 7 actions', () => {
60
+ expect(tool.getSupportedActions()).toEqual(
61
+ ['add', 'update', 'delete', 'list', 'read', 'search', 'stats']
62
+ );
63
+ });
64
+
65
+ test('getRequiredParameters returns action', () => {
66
+ expect(tool.getRequiredParameters()).toEqual(['action']);
67
+ });
68
+
69
+ test('getCapabilities includes persistent flag', () => {
70
+ const caps = tool.getCapabilities();
71
+ expect(caps.persistent).toBe(true);
72
+ expect(caps.actions).toEqual(tool.getSupportedActions());
73
+ });
74
+
75
+ test('getParameterSchema has action enum', () => {
76
+ const schema = tool.getParameterSchema();
77
+ expect(schema.properties.action.enum).toEqual(tool.getSupportedActions());
78
+ });
79
+
80
+ test('parseParameters returns content as-is', () => {
81
+ const input = { action: 'add', title: 'test' };
82
+ expect(tool.parseParameters(input)).toBe(input);
83
+ });
84
+
85
+ test('validateParameterTypes catches invalid action type', () => {
86
+ const result = tool.validateParameterTypes({ action: 123 });
87
+ expect(result.valid).toBe(false);
88
+ });
89
+
90
+ test('validateParameterTypes catches invalid level', () => {
91
+ const result = tool.validateParameterTypes({ level: 'invalid' });
92
+ expect(result.valid).toBe(false);
93
+ });
94
+
95
+ test('validateParameterTypes accepts valid params', () => {
96
+ const result = tool.validateParameterTypes({ action: 'add', title: 'test', level: 'titles' });
97
+ expect(result.valid).toBe(true);
98
+ });
99
+
100
+ test('customValidateParameters rejects unknown action', () => {
101
+ const result = tool.customValidateParameters({ action: 'unknown' });
102
+ expect(result.valid).toBe(false);
103
+ });
104
+
105
+ test('customValidateParameters requires title and content for add', () => {
106
+ const result = tool.customValidateParameters({ action: 'add' });
107
+ expect(result.valid).toBe(false);
108
+ expect(result.errors.some(e => e.includes('title'))).toBe(true);
109
+ expect(result.errors.some(e => e.includes('content'))).toBe(true);
110
+ });
111
+
112
+ test('customValidateParameters requires id for update', () => {
113
+ const result = tool.customValidateParameters({ action: 'update' });
114
+ expect(result.valid).toBe(false);
115
+ });
116
+
117
+ test('customValidateParameters requires id for delete', () => {
118
+ const result = tool.customValidateParameters({ action: 'delete' });
119
+ expect(result.valid).toBe(false);
120
+ });
121
+
122
+ test('customValidateParameters requires id for read', () => {
123
+ const result = tool.customValidateParameters({ action: 'read' });
124
+ expect(result.valid).toBe(false);
125
+ });
126
+
127
+ test('customValidateParameters requires query for search', () => {
128
+ const result = tool.customValidateParameters({ action: 'search' });
129
+ expect(result.valid).toBe(false);
130
+ });
131
+
132
+ test('customValidateParameters enforces length limits', () => {
133
+ const result = tool.customValidateParameters({
134
+ action: 'add',
135
+ title: 'a'.repeat(201),
136
+ content: 'c'.repeat(10001),
137
+ description: 'd'.repeat(501)
138
+ });
139
+ expect(result.valid).toBe(false);
140
+ expect(result.errors.length).toBe(3);
141
+ });
142
+
143
+ test('execute throws without agentId', async () => {
144
+ await expect(tool.execute({ action: 'stats' }, {}))
145
+ .rejects.toThrow('Agent ID is required');
146
+ });
147
+
148
+ test('execute add action creates memory', async () => {
149
+ mockMemoryService.addMemory.mockResolvedValue({
150
+ id: 'mem-1', title: 'Test', createdAt: '2025-01-01', expiration: null
151
+ });
152
+
153
+ const result = await tool.execute(
154
+ { action: 'add', title: 'Test', content: 'content', description: 'desc' },
155
+ context
156
+ );
157
+
158
+ expect(result.success).toBe(true);
159
+ expect(result.action).toBe('add');
160
+ expect(result.memory.id).toBe('mem-1');
161
+ expect(mockMemoryService.addMemory).toHaveBeenCalledWith('agent-1', expect.objectContaining({ title: 'Test' }));
162
+ });
163
+
164
+ test('execute update action modifies memory', async () => {
165
+ mockMemoryService.updateMemory.mockResolvedValue({
166
+ id: 'mem-1', title: 'Updated', updatedAt: '2025-01-02'
167
+ });
168
+
169
+ const result = await tool.execute(
170
+ { action: 'update', id: 'mem-1', title: 'Updated' },
171
+ context
172
+ );
173
+
174
+ expect(result.success).toBe(true);
175
+ expect(result.action).toBe('update');
176
+ });
177
+
178
+ test('execute update returns failure when memory not found', async () => {
179
+ mockMemoryService.updateMemory.mockResolvedValue(null);
180
+
181
+ const result = await tool.execute(
182
+ { action: 'update', id: 'nonexistent', title: 'x' },
183
+ context
184
+ );
185
+
186
+ expect(result.success).toBe(false);
187
+ expect(result.message).toContain('not found');
188
+ });
189
+
190
+ test('execute delete action removes memory', async () => {
191
+ mockMemoryService.deleteMemory.mockResolvedValue(true);
192
+
193
+ const result = await tool.execute({ action: 'delete', id: 'mem-1' }, context);
194
+
195
+ expect(result.success).toBe(true);
196
+ expect(result.action).toBe('delete');
197
+ });
198
+
199
+ test('execute delete returns failure when memory not found', async () => {
200
+ mockMemoryService.deleteMemory.mockResolvedValue(false);
201
+
202
+ const result = await tool.execute({ action: 'delete', id: 'nonexistent' }, context);
203
+
204
+ expect(result.success).toBe(false);
205
+ });
206
+
207
+ test('execute list action returns memories', async () => {
208
+ mockMemoryService.listMemories.mockResolvedValue({
209
+ count: 2,
210
+ grouped: { '2025-01-01': [{ id: 'mem-1' }, { id: 'mem-2' }] }
211
+ });
212
+
213
+ const result = await tool.execute({ action: 'list', level: 'titles' }, context);
214
+
215
+ expect(result.success).toBe(true);
216
+ expect(result.totalMemories).toBe(2);
217
+ expect(result.level).toBe('titles');
218
+ });
219
+
220
+ test('execute list with default level', async () => {
221
+ mockMemoryService.listMemories.mockResolvedValue({ count: 0, grouped: {} });
222
+
223
+ const result = await tool.execute({ action: 'list' }, context);
224
+
225
+ expect(result.level).toBe('titles');
226
+ expect(result.message).toContain('No memories');
227
+ });
228
+
229
+ test('execute read action loads memory', async () => {
230
+ mockMemoryService.readMemory.mockResolvedValue({
231
+ id: 'mem-1', title: 'Test', description: 'desc', content: 'full content',
232
+ createdAt: '2025-01-01', updatedAt: null, expiration: null, accessCount: 3
233
+ });
234
+
235
+ const result = await tool.execute({ action: 'read', id: 'mem-1' }, context);
236
+
237
+ expect(result.success).toBe(true);
238
+ expect(result.memory.content).toBe('full content');
239
+ });
240
+
241
+ test('execute read returns failure when not found', async () => {
242
+ mockMemoryService.readMemory.mockResolvedValue(null);
243
+
244
+ const result = await tool.execute({ action: 'read', id: 'nonexistent' }, context);
245
+
246
+ expect(result.success).toBe(false);
247
+ });
248
+
249
+ test('execute search action returns results', async () => {
250
+ mockMemoryService.searchMemories.mockResolvedValue([
251
+ { id: 'mem-1', title: 'Match' }
252
+ ]);
253
+
254
+ const result = await tool.execute({ action: 'search', query: 'Match' }, context);
255
+
256
+ expect(result.success).toBe(true);
257
+ expect(result.results.length).toBe(1);
258
+ expect(result.query).toBe('Match');
259
+ });
260
+
261
+ test('execute search with no results', async () => {
262
+ mockMemoryService.searchMemories.mockResolvedValue([]);
263
+
264
+ const result = await tool.execute({ action: 'search', query: 'nothing' }, context);
265
+
266
+ expect(result.success).toBe(true);
267
+ expect(result.results.length).toBe(0);
268
+ expect(result.message).toContain('No memories found');
269
+ });
270
+
271
+ test('execute stats action returns statistics', async () => {
272
+ mockMemoryService.getMemoryStats.mockResolvedValue({
273
+ totalMemories: 5, totalAccessCount: 20
274
+ });
275
+
276
+ const result = await tool.execute({ action: 'stats' }, context);
277
+
278
+ expect(result.success).toBe(true);
279
+ expect(result.stats.totalMemories).toBe(5);
280
+ expect(result.message).toContain('5 total memories');
281
+ });
282
+
283
+ test('execute throws on unknown action', async () => {
284
+ await expect(tool.execute({ action: 'unknown' }, context))
285
+ .rejects.toThrow('Unknown action');
286
+ });
287
+
288
+ test('execute logs and re-throws service errors', async () => {
289
+ mockMemoryService.addMemory.mockRejectedValue(new Error('DB error'));
290
+
291
+ await expect(
292
+ tool.execute({ action: 'add', title: 'x', content: 'y' }, context)
293
+ ).rejects.toThrow('DB error');
294
+
295
+ expect(logger.error).toHaveBeenCalled();
296
+ });
297
+ });
@@ -0,0 +1,282 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock fs/promises before import
5
+ const mockFs = {
6
+ stat: jest.fn(),
7
+ readFile: jest.fn(),
8
+ readdir: jest.fn()
9
+ };
10
+
11
+ jest.unstable_mockModule('fs', () => ({
12
+ promises: mockFs,
13
+ default: { promises: mockFs }
14
+ }));
15
+
16
+ // Mock constants
17
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
18
+ TOOL_STATUS: { PENDING: 'pending', EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed' },
19
+ OPERATION_STATUS: { EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed', NOT_FOUND: 'not_found' },
20
+ ERROR_TYPES: {},
21
+ SYSTEM_DEFAULTS: { MAX_TOOL_EXECUTION_TIME: 300000 }
22
+ }));
23
+
24
+ // Mock tagParser
25
+ jest.unstable_mockModule('../../utilities/tagParser.js', () => ({
26
+ default: { extractContent: jest.fn(() => []) }
27
+ }));
28
+
29
+ const { default: SeekTool } = await import('../seekTool.js');
30
+
31
+ describe('SeekTool', () => {
32
+ let tool;
33
+ let logger;
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ logger = createMockLogger();
38
+ tool = new SeekTool({}, logger);
39
+ });
40
+
41
+ test('constructor sets metadata correctly', () => {
42
+ expect(tool.id).toBe('seek');
43
+ expect(tool.requiresProject).toBe(true);
44
+ expect(tool.isAsync).toBe(true);
45
+ expect(tool.timeout).toBe(120000);
46
+ });
47
+
48
+ test('getDescription returns seek description', () => {
49
+ const desc = tool.getDescription();
50
+ expect(desc).toContain('Seek Tool');
51
+ expect(desc).toContain('filePaths');
52
+ expect(desc).toContain('searchTerms');
53
+ });
54
+
55
+ test('getRequiredParameters returns filePaths and searchTerms', () => {
56
+ expect(tool.getRequiredParameters()).toEqual(['filePaths', 'searchTerms']);
57
+ });
58
+
59
+ test('parseParameters parses JSON content', () => {
60
+ const content = JSON.stringify({
61
+ filePaths: ['src/*.js'],
62
+ searchTerms: ['import React']
63
+ });
64
+ const result = tool.parseParameters(content);
65
+ expect(result.filePaths).toEqual(['src/*.js']);
66
+ expect(result.searchTerms).toEqual(['import React']);
67
+ });
68
+
69
+ test('parseParameters parses XML content', () => {
70
+ const content = `
71
+ <in-files>
72
+ src/index.js
73
+ src/app.js
74
+ </in-files>
75
+ <search-terms>
76
+ <term>useState</term>
77
+ <term>useEffect</term>
78
+ </search-terms>
79
+ `;
80
+ const result = tool.parseParameters(content);
81
+ expect(result.filePaths).toEqual(['src/index.js', 'src/app.js']);
82
+ expect(result.searchTerms).toEqual(['useState', 'useEffect']);
83
+ });
84
+
85
+ test('parseParameters returns parseError on invalid content', () => {
86
+ const result = tool.parseParameters('{ broken json');
87
+ expect(result).toHaveProperty('parseError');
88
+ });
89
+
90
+ test('customValidateParameters rejects empty filePaths', () => {
91
+ const result = tool.customValidateParameters({ filePaths: [], searchTerms: ['foo'] });
92
+ expect(result.valid).toBe(false);
93
+ });
94
+
95
+ test('customValidateParameters rejects empty searchTerms', () => {
96
+ const result = tool.customValidateParameters({ filePaths: ['a.js'], searchTerms: [] });
97
+ expect(result.valid).toBe(false);
98
+ });
99
+
100
+ test('customValidateParameters rejects path traversal', () => {
101
+ const result = tool.customValidateParameters({
102
+ filePaths: ['../../etc/passwd'],
103
+ searchTerms: ['test']
104
+ });
105
+ expect(result.valid).toBe(false);
106
+ expect(result.errors.some(e => e.includes('traversal'))).toBe(true);
107
+ });
108
+
109
+ test('customValidateParameters accepts valid params', () => {
110
+ const result = tool.customValidateParameters({
111
+ filePaths: ['src/index.js'],
112
+ searchTerms: ['import']
113
+ });
114
+ expect(result.valid).toBe(true);
115
+ });
116
+
117
+ test('execute searches file and returns matches', async () => {
118
+ const fileContent = 'line 1\nimport React from "react";\nline 3\nimport useState from "react";\n';
119
+
120
+ mockFs.stat.mockResolvedValue({
121
+ isFile: () => true,
122
+ isDirectory: () => false,
123
+ size: 100
124
+ });
125
+ mockFs.readFile.mockResolvedValue(fileContent);
126
+
127
+ const result = await tool.execute(
128
+ { filePaths: ['src/app.js'], searchTerms: ['import React'] },
129
+ { projectDir: '/project', agentId: 'agent-1' }
130
+ );
131
+
132
+ expect(result.success).toBe(true);
133
+ expect(result.totalMatches).toBe(1);
134
+ expect(result.filesSearched).toBe(1);
135
+ });
136
+
137
+ test('execute handles file not found', async () => {
138
+ mockFs.stat.mockRejectedValue({ code: 'ENOENT', message: 'not found' });
139
+
140
+ const result = await tool.execute(
141
+ { filePaths: ['missing.js'], searchTerms: ['test'] },
142
+ { projectDir: '/project', agentId: 'agent-1' }
143
+ );
144
+
145
+ expect(result.success).toBe(true);
146
+ expect(result.filesNotFound).toBe(1);
147
+ expect(result.totalMatches).toBe(0);
148
+ });
149
+
150
+ test('execute returns no matches when search term not found', async () => {
151
+ mockFs.stat.mockResolvedValue({
152
+ isFile: () => true,
153
+ isDirectory: () => false,
154
+ size: 50
155
+ });
156
+ mockFs.readFile.mockResolvedValue('no matching content here\n');
157
+
158
+ const result = await tool.execute(
159
+ { filePaths: ['src/app.js'], searchTerms: ['nonexistent'] },
160
+ { projectDir: '/project', agentId: 'agent-1' }
161
+ );
162
+
163
+ expect(result.success).toBe(true);
164
+ expect(result.totalMatches).toBe(0);
165
+ });
166
+
167
+ test('execute skips binary files', async () => {
168
+ mockFs.stat.mockResolvedValue({
169
+ isFile: () => true,
170
+ isDirectory: () => false,
171
+ size: 100
172
+ });
173
+
174
+ const result = await tool.execute(
175
+ { filePaths: ['image.png'], searchTerms: ['test'] },
176
+ { projectDir: '/project', agentId: 'agent-1' }
177
+ );
178
+
179
+ expect(result.success).toBe(true);
180
+ // Binary file is resolved but skipped during search
181
+ expect(mockFs.readFile).not.toHaveBeenCalled();
182
+ });
183
+
184
+ test('execute skips oversized files', async () => {
185
+ mockFs.stat
186
+ .mockResolvedValueOnce({ isFile: () => true, isDirectory: () => false, size: 100 }) // resolveFilePaths
187
+ .mockResolvedValueOnce({ size: 20 * 1024 * 1024 }); // searchFiles - too large
188
+
189
+ const result = await tool.execute(
190
+ { filePaths: ['large.js'], searchTerms: ['test'] },
191
+ { projectDir: '/project', agentId: 'agent-1' }
192
+ );
193
+
194
+ expect(result.success).toBe(true);
195
+ expect(result.filesWithErrors).toBe(1);
196
+ });
197
+
198
+ test('execute truncates long line content around match', async () => {
199
+ const longLine = 'a'.repeat(100) + 'MATCH_HERE' + 'b'.repeat(200);
200
+ mockFs.stat.mockResolvedValue({
201
+ isFile: () => true,
202
+ isDirectory: () => false,
203
+ size: 500
204
+ });
205
+ mockFs.readFile.mockResolvedValue(longLine + '\n');
206
+
207
+ const result = await tool.execute(
208
+ { filePaths: ['file.js'], searchTerms: ['MATCH_HERE'] },
209
+ { projectDir: '/project', agentId: 'agent-1' }
210
+ );
211
+
212
+ expect(result.success).toBe(true);
213
+ expect(result.totalMatches).toBe(1);
214
+ });
215
+
216
+ test('execute uses directoryAccess when provided', async () => {
217
+ mockFs.stat.mockResolvedValue({
218
+ isFile: () => true,
219
+ isDirectory: () => false,
220
+ size: 50
221
+ });
222
+ mockFs.readFile.mockResolvedValue('hello world\n');
223
+
224
+ const result = await tool.execute(
225
+ { filePaths: ['test.js'], searchTerms: ['hello'] },
226
+ {
227
+ projectDir: '/project',
228
+ agentId: 'agent-1',
229
+ directoryAccess: {
230
+ workingDirectory: '/project',
231
+ readOnlyDirectories: ['/shared'],
232
+ writeEnabledDirectories: ['/output']
233
+ }
234
+ }
235
+ );
236
+
237
+ expect(result.success).toBe(true);
238
+ });
239
+
240
+ test('matchesPattern handles wildcard patterns', () => {
241
+ expect(tool.matchesPattern('file.js', '*.js')).toBe(true);
242
+ expect(tool.matchesPattern('file.ts', '*.js')).toBe(false);
243
+ expect(tool.matchesPattern('test.spec.js', '*.spec.js')).toBe(true);
244
+ });
245
+
246
+ test('shouldSkipDirectory returns true for node_modules', () => {
247
+ expect(tool.shouldSkipDirectory('node_modules')).toBe(true);
248
+ expect(tool.shouldSkipDirectory('.git')).toBe(true);
249
+ expect(tool.shouldSkipDirectory('src')).toBe(false);
250
+ });
251
+
252
+ test('shouldSkipFile returns true for binary extensions', () => {
253
+ expect(tool.shouldSkipFile('image.png')).toBe(true);
254
+ expect(tool.shouldSkipFile('script.js')).toBe(false);
255
+ });
256
+
257
+ test('formatResults with no matches', () => {
258
+ const output = tool.formatResults([], [], [], 5);
259
+ expect(output).toContain('No matches found');
260
+ });
261
+
262
+ test('formatResults with matches', () => {
263
+ const matches = [
264
+ { term: 'foo', filePath: 'a.js', lineNumber: 10, lineContent: 'const foo = 1;' }
265
+ ];
266
+ const output = tool.formatResults(matches, [], [], 1);
267
+ expect(output).toContain('SEARCH RESULTS');
268
+ expect(output).toContain('foo');
269
+ expect(output).toContain('a.js:10');
270
+ });
271
+
272
+ test('formatResults with not found and error files', () => {
273
+ const output = tool.formatResults(
274
+ [],
275
+ [{ filePath: 'err.js', error: 'read error' }],
276
+ ['missing.js (ENOENT)'],
277
+ 0
278
+ );
279
+ expect(output).toContain('FILES NOT FOUND');
280
+ expect(output).toContain('FILES WITH ERRORS');
281
+ });
282
+ });