onbuzz 3.6.1 → 3.6.3

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 (84) 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__/fileSystemTool.test.js +717 -0
  53. package/src/tools/__tests__/fileTreeTool.test.js +274 -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
  84. /package/src/tools/{filesystemTool.js → fileSystemTool.js} +0 -0
@@ -0,0 +1,420 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock constants before importing BaseTool
5
+ const TOOL_STATUS = {
6
+ PENDING: 'pending',
7
+ EXECUTING: 'executing',
8
+ COMPLETED: 'completed',
9
+ FAILED: 'failed'
10
+ };
11
+
12
+ const OPERATION_STATUS = {
13
+ EXECUTING: 'executing',
14
+ COMPLETED: 'completed',
15
+ FAILED: 'failed',
16
+ NOT_FOUND: 'not_found'
17
+ };
18
+
19
+ const SYSTEM_DEFAULTS = {
20
+ MAX_TOOL_EXECUTION_TIME: 300000
21
+ };
22
+
23
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
24
+ TOOL_STATUS,
25
+ OPERATION_STATUS,
26
+ ERROR_TYPES: {},
27
+ SYSTEM_DEFAULTS
28
+ }));
29
+
30
+ const { BaseTool, ToolsRegistry } = await import('../baseTool.js');
31
+
32
+ // Concrete subclass for testing
33
+ class TestTool extends BaseTool {
34
+ getDescription() { return 'Test tool for testing\nSecond line of description'; }
35
+ getSummary() { return 'Test tool'; }
36
+ parseParameters(content) {
37
+ if (typeof content === 'string' && content.trim().startsWith('{')) return JSON.parse(content);
38
+ return { raw: content };
39
+ }
40
+ async execute(params, context) { return { success: true, data: params }; }
41
+ }
42
+
43
+ // Subclass that throws on execute
44
+ class FailingTool extends BaseTool {
45
+ getDescription() { return 'Failing tool'; }
46
+ parseParameters(content) { return {}; }
47
+ async execute() { throw new Error('Intentional failure'); }
48
+ }
49
+
50
+ // Subclass with required params
51
+ class StrictTool extends BaseTool {
52
+ getDescription() { return 'Strict tool'; }
53
+ parseParameters(content) { return JSON.parse(content); }
54
+ async execute(params) { return params; }
55
+ getRequiredParameters() { return ['name']; }
56
+ }
57
+
58
+ describe('BaseTool', () => {
59
+ let tool;
60
+ let logger;
61
+
62
+ beforeEach(() => {
63
+ logger = createMockLogger();
64
+ tool = new TestTool({}, logger);
65
+ });
66
+
67
+ test('constructor sets default values', () => {
68
+ const t = new TestTool();
69
+ expect(t.isEnabled).toBe(true);
70
+ expect(t.usageCount).toBe(0);
71
+ expect(t.lastUsed).toBeNull();
72
+ expect(t.operationHistory).toEqual([]);
73
+ expect(t.activeOperations.size).toBe(0);
74
+ expect(t.requiresProject).toBe(false);
75
+ expect(t.isAsync).toBe(false);
76
+ expect(t.builtinDelay).toBe(0);
77
+ });
78
+
79
+ test('constructor respects config.enabled = false', () => {
80
+ const t = new TestTool({ enabled: false });
81
+ expect(t.isEnabled).toBe(false);
82
+ });
83
+
84
+ test('constructor uses config.timeout when provided', () => {
85
+ const t = new TestTool({ timeout: 5000 });
86
+ expect(t.timeout).toBe(5000);
87
+ });
88
+
89
+ test('id is derived from class name', () => {
90
+ expect(tool.id).toBe('test');
91
+ });
92
+
93
+ test('getCapabilities returns correct object', () => {
94
+ const caps = tool.getCapabilities();
95
+ expect(caps.id).toBe('test');
96
+ expect(caps.enabled).toBe(true);
97
+ expect(caps.timeout).toBe(300000);
98
+ expect(caps.supportedActions).toEqual(['execute']);
99
+ expect(caps).toHaveProperty('parameterSchema');
100
+ expect(caps).toHaveProperty('maxConcurrentOperations');
101
+ });
102
+
103
+ test('getSupportedActions returns default array', () => {
104
+ expect(tool.getSupportedActions()).toEqual(['execute']);
105
+ });
106
+
107
+ test('getSummary returns first line of description', () => {
108
+ expect(tool.getSummary()).toBe('Test tool');
109
+ });
110
+
111
+ test('getSummary returns fallback when getDescription throws', () => {
112
+ const bare = new BaseTool();
113
+ const summary = bare.getSummary();
114
+ expect(summary).toContain('tool');
115
+ });
116
+
117
+ test('getParameterSchema returns default schema', () => {
118
+ const schema = tool.getParameterSchema();
119
+ expect(schema.type).toBe('object');
120
+ expect(schema).toHaveProperty('properties');
121
+ expect(schema).toHaveProperty('required');
122
+ });
123
+
124
+ test('getUsageStats returns stats object with all fields', () => {
125
+ const stats = tool.getUsageStats();
126
+ expect(stats.toolId).toBe('test');
127
+ expect(stats.usageCount).toBe(0);
128
+ expect(stats.lastUsed).toBeNull();
129
+ expect(stats.activeOperations).toBe(0);
130
+ expect(stats.totalOperations).toBe(0);
131
+ expect(stats.averageExecutionTime).toBe(0);
132
+ expect(stats.successRate).toBe(0);
133
+ expect(stats.isEnabled).toBe(true);
134
+ });
135
+
136
+ test('enable and disable toggle isEnabled', () => {
137
+ tool.disable();
138
+ expect(tool.isEnabled).toBe(false);
139
+ expect(logger.info).toHaveBeenCalled();
140
+ tool.enable();
141
+ expect(tool.isEnabled).toBe(true);
142
+ });
143
+
144
+ test('resetStats clears counters', () => {
145
+ tool.usageCount = 5;
146
+ tool.lastUsed = '2025-01-01';
147
+ tool.operationHistory = [{ id: 'x' }];
148
+ tool.resetStats();
149
+ expect(tool.usageCount).toBe(0);
150
+ expect(tool.lastUsed).toBeNull();
151
+ expect(tool.operationHistory).toEqual([]);
152
+ });
153
+
154
+ test('executeWithLifecycle calls execute and tracks stats', async () => {
155
+ const result = await tool.executeWithLifecycle({ foo: 'bar' }, {});
156
+ expect(result.success).toBe(true);
157
+ expect(result.result).toEqual({ success: true, data: { foo: 'bar' } });
158
+ expect(result.toolId).toBe('test');
159
+ expect(result.executionTime).toBeGreaterThanOrEqual(0);
160
+ expect(result).toHaveProperty('operationId');
161
+ expect(tool.usageCount).toBe(1);
162
+ expect(tool.lastUsed).toBeTruthy();
163
+ expect(tool.operationHistory.length).toBe(1);
164
+ expect(tool.operationHistory[0].status).toBe('completed');
165
+ });
166
+
167
+ test('executeWithLifecycle throws when tool is disabled', async () => {
168
+ tool.disable();
169
+ await expect(tool.executeWithLifecycle({}, {})).rejects.toThrow('disabled');
170
+ });
171
+
172
+ test('executeWithLifecycle throws on concurrent operation limit', async () => {
173
+ tool.maxConcurrentOperations = 0;
174
+ await expect(tool.executeWithLifecycle({}, {})).rejects.toThrow('concurrent');
175
+ });
176
+
177
+ test('executeWithLifecycle handles execute error and records failure', async () => {
178
+ const failTool = new FailingTool({}, logger);
179
+ await expect(failTool.executeWithLifecycle({}, {})).rejects.toThrow('Intentional failure');
180
+ expect(failTool.operationHistory.length).toBe(1);
181
+ expect(failTool.operationHistory[0].status).toBe('failed');
182
+ expect(failTool.operationHistory[0].error).toBe('Intentional failure');
183
+ expect(failTool.activeOperations.size).toBe(0);
184
+ });
185
+
186
+ test('executeWithLifecycle handles timeout', async () => {
187
+ const slowTool = new TestTool({ timeout: 10 }, logger);
188
+ slowTool.execute = async () => new Promise(resolve => setTimeout(resolve, 500));
189
+ await expect(slowTool.executeWithLifecycle({}, {})).rejects.toThrow('timed out');
190
+ }, 5000);
191
+
192
+ test('validateParameters rejects non-object params', () => {
193
+ expect(tool.validateParameters(null).valid).toBe(false);
194
+ expect(tool.validateParameters('string').valid).toBe(false);
195
+ });
196
+
197
+ test('validateParameters checks required parameters', () => {
198
+ const strictTool = new StrictTool({}, logger);
199
+ const result = strictTool.validateParameters({ other: 'value' });
200
+ expect(result.valid).toBe(false);
201
+ expect(result.error).toContain('name');
202
+ });
203
+
204
+ test('validateParameters passes valid params', () => {
205
+ const strictTool = new StrictTool({}, logger);
206
+ const result = strictTool.validateParameters({ name: 'test' });
207
+ expect(result.valid).toBe(true);
208
+ });
209
+
210
+ test('getStatus returns NOT_FOUND for unknown operation', async () => {
211
+ const status = await tool.getStatus('nonexistent');
212
+ expect(status.status).toBe('not_found');
213
+ });
214
+
215
+ test('getStatus returns history entry for completed operation', async () => {
216
+ await tool.executeWithLifecycle({}, {});
217
+ const opId = tool.operationHistory[0].id;
218
+ const status = await tool.getStatus(opId);
219
+ expect(status.status).toBe('completed');
220
+ });
221
+
222
+ test('sanitizeContext removes sensitive fields and truncates content', () => {
223
+ const ctx = {
224
+ apiKeys: 'secret',
225
+ secrets: 'hidden',
226
+ passwords: 'pass',
227
+ content: 'a'.repeat(600),
228
+ projectDir: '/test'
229
+ };
230
+ const sanitized = tool.sanitizeContext(ctx);
231
+ expect(sanitized.apiKeys).toBeUndefined();
232
+ expect(sanitized.secrets).toBeUndefined();
233
+ expect(sanitized.passwords).toBeUndefined();
234
+ expect(sanitized.content).toContain('[truncated]');
235
+ expect(sanitized.content.length).toBeLessThan(600);
236
+ expect(sanitized.projectDir).toBe('/test');
237
+ });
238
+
239
+ test('cleanupHistory trims beyond 100 entries', () => {
240
+ tool.operationHistory = Array.from({ length: 120 }, (_, i) => ({ id: i }));
241
+ tool.cleanupHistory();
242
+ expect(tool.operationHistory.length).toBe(100);
243
+ expect(tool.operationHistory[0].id).toBe(20);
244
+ });
245
+
246
+ test('cleanupHistory does nothing under limit', () => {
247
+ tool.operationHistory = [{ id: 1 }, { id: 2 }];
248
+ tool.cleanupHistory();
249
+ expect(tool.operationHistory.length).toBe(2);
250
+ });
251
+
252
+ test('calculateSuccessRate returns correct percentage', () => {
253
+ tool.operationHistory = [
254
+ { status: 'completed' },
255
+ { status: 'completed' },
256
+ { status: 'failed' },
257
+ { status: 'completed' }
258
+ ];
259
+ expect(tool.calculateSuccessRate()).toBe(75);
260
+ });
261
+
262
+ test('calculateSuccessRate returns 0 with no history', () => {
263
+ expect(tool.calculateSuccessRate()).toBe(0);
264
+ });
265
+
266
+ test('calculateAverageExecutionTime computes correctly', () => {
267
+ tool.operationHistory = [
268
+ { status: 'completed', executionTime: 100 },
269
+ { status: 'completed', executionTime: 200 },
270
+ { status: 'failed', executionTime: 999 }
271
+ ];
272
+ expect(tool.calculateAverageExecutionTime()).toBe(150);
273
+ });
274
+
275
+ test('calculateAverageExecutionTime returns 0 with no completed ops', () => {
276
+ tool.operationHistory = [{ status: 'failed' }];
277
+ expect(tool.calculateAverageExecutionTime()).toBe(0);
278
+ });
279
+
280
+ test('base class getDescription throws', () => {
281
+ const base = new BaseTool();
282
+ expect(() => base.getDescription()).toThrow('must implement getDescription');
283
+ });
284
+
285
+ test('base class parseParameters throws', () => {
286
+ const base = new BaseTool();
287
+ expect(() => base.parseParameters('test')).toThrow('must implement parseParameters');
288
+ });
289
+
290
+ test('base class execute throws', async () => {
291
+ const base = new BaseTool();
292
+ await expect(base.execute({}, {})).rejects.toThrow('must implement execute');
293
+ });
294
+ });
295
+
296
+ describe('ToolsRegistry', () => {
297
+ let registry;
298
+ let logger;
299
+
300
+ beforeEach(() => {
301
+ logger = createMockLogger();
302
+ registry = new ToolsRegistry(logger);
303
+ });
304
+
305
+ test('registerTool adds a tool', async () => {
306
+ await registry.registerTool(TestTool);
307
+ expect(registry.listTools()).toContain('test');
308
+ });
309
+
310
+ test('getTool retrieves by ID', async () => {
311
+ await registry.registerTool(TestTool);
312
+ const tool = registry.getTool('test');
313
+ expect(tool).toBeInstanceOf(BaseTool);
314
+ });
315
+
316
+ test('getTool returns null for unknown ID', () => {
317
+ expect(registry.getTool('unknown')).toBeNull();
318
+ });
319
+
320
+ test('listTools returns registered IDs', async () => {
321
+ await registry.registerTool(TestTool);
322
+ const tools = registry.listTools();
323
+ expect(Array.isArray(tools)).toBe(true);
324
+ expect(tools).toContain('test');
325
+ });
326
+
327
+ test('executeToolSecurely validates and delegates', async () => {
328
+ await registry.registerTool(TestTool);
329
+ const result = await registry.executeToolSecurely('test', {}, {});
330
+ expect(result.success).toBe(true);
331
+ });
332
+
333
+ test('executeToolSecurely throws for unknown tool', async () => {
334
+ await expect(registry.executeToolSecurely('nope', {}, {})).rejects.toThrow('not found');
335
+ });
336
+
337
+ test('executeToolSecurely throws for disabled tool', async () => {
338
+ await registry.registerTool(TestTool);
339
+ registry.getTool('test').disable();
340
+ await expect(registry.executeToolSecurely('test', {}, {})).rejects.toThrow('disabled');
341
+ });
342
+
343
+ test('generateToolDescriptionsForPrompt returns formatted string', async () => {
344
+ await registry.registerTool(TestTool);
345
+ const desc = registry.generateToolDescriptionsForPrompt();
346
+ expect(desc).toContain('AVAILABLE TOOLS');
347
+ expect(desc).toContain('TOOL INVOCATION SYNTAX');
348
+ });
349
+
350
+ test('generateToolDescriptionsForPrompt returns empty for no matching tools', () => {
351
+ const desc = registry.generateToolDescriptionsForPrompt(['nonexistent']);
352
+ expect(desc).toBe('');
353
+ });
354
+
355
+ test('generateToolDescriptionsForPrompt compact mode', async () => {
356
+ await registry.registerTool(TestTool);
357
+ const desc = registry.generateToolDescriptionsForPrompt([], { compact: true });
358
+ expect(desc).toContain('test');
359
+ });
360
+
361
+ test('generateToolDescriptionsForPrompt layered mode', async () => {
362
+ await registry.registerTool(TestTool);
363
+ const desc = registry.generateToolDescriptionsForPrompt([], { layered: true });
364
+ expect(desc).toContain('HOW TO GET TOOL DOCUMENTATION');
365
+ });
366
+
367
+ test('enhanceSystemPrompt appends tool docs', async () => {
368
+ await registry.registerTool(TestTool);
369
+ const enhanced = registry.enhanceSystemPrompt('Base prompt.', []);
370
+ expect(enhanced).toContain('Base prompt.');
371
+ expect(enhanced).toContain('AVAILABLE TOOLS');
372
+ });
373
+
374
+ test('enhanceSystemPrompt returns empty prompt when no tools match', () => {
375
+ const enhanced = registry.enhanceSystemPrompt('', ['nonexistent']);
376
+ expect(enhanced).toBe('');
377
+ });
378
+
379
+ test('enhanceSystemPrompt replaces existing tool section', async () => {
380
+ await registry.registerTool(TestTool);
381
+ const existing = 'Prefix\n## AVAILABLE TOOLS\nOld content\n## OTHER SECTION';
382
+ const enhanced = registry.enhanceSystemPrompt(existing, []);
383
+ expect(enhanced).toContain('Prefix');
384
+ expect(enhanced).not.toContain('Old content');
385
+ });
386
+
387
+ test('getRegistryStats returns correct counts', async () => {
388
+ await registry.registerTool(TestTool);
389
+ const stats = registry.getRegistryStats();
390
+ expect(stats.totalTools).toBe(1);
391
+ expect(stats.enabledTools).toBe(1);
392
+ expect(stats.totalOperations).toBe(0);
393
+ expect(stats.activeOperations).toBe(0);
394
+ });
395
+
396
+ test('getToolCapabilities returns enabled tools only', async () => {
397
+ await registry.registerTool(TestTool);
398
+ const caps = registry.getToolCapabilities();
399
+ expect(caps).toHaveProperty('test');
400
+ expect(caps.test).toHaveProperty('description');
401
+ expect(caps.test).toHaveProperty('usageStats');
402
+ });
403
+
404
+ test('getAvailableToolsForUI returns sorted array with expected shape', async () => {
405
+ await registry.registerTool(TestTool);
406
+ const tools = registry.getAvailableToolsForUI();
407
+ expect(Array.isArray(tools)).toBe(true);
408
+ expect(tools.length).toBe(1);
409
+ expect(tools[0]).toHaveProperty('id');
410
+ expect(tools[0]).toHaveProperty('name');
411
+ expect(tools[0]).toHaveProperty('category');
412
+ expect(tools[0]).toHaveProperty('enabled');
413
+ expect(tools[0]).toHaveProperty('className');
414
+ });
415
+
416
+ test('discoverTools returns 0', async () => {
417
+ const count = await registry.discoverTools('/some/dir');
418
+ expect(count).toBe(0);
419
+ });
420
+ });