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,342 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock dependencies before importing
5
+ jest.unstable_mockModule('../../utilities/tagParser.js', () => ({
6
+ default: {
7
+ extractContent: jest.fn((content, tag) => {
8
+ const regex = new RegExp(`<${tag}>(.*?)</${tag}>`, 'gs');
9
+ const matches = [];
10
+ let match;
11
+ while ((match = regex.exec(content)) !== null) {
12
+ matches.push(match[1]);
13
+ }
14
+ return matches;
15
+ })
16
+ }
17
+ }));
18
+
19
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
20
+ TOOL_STATUS: { PENDING: 'pending', EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed' },
21
+ SYSTEM_DEFAULTS: { MAX_PAUSE_DURATION: 300, MAX_TOOL_EXECUTION_TIME: 300000 },
22
+ AGENT_STATUS: { ACTIVE: 'active', IDLE: 'idle', BUSY: 'busy', PAUSED: 'paused' },
23
+ OPERATION_STATUS: { EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed' },
24
+ ERROR_TYPES: {}
25
+ }));
26
+
27
+ const { default: AgentDelayTool } = await import('../agentDelayTool.js');
28
+
29
+ describe('AgentDelayTool', () => {
30
+ let tool;
31
+ let logger;
32
+ let mockAgentPool;
33
+
34
+ beforeEach(() => {
35
+ logger = createMockLogger();
36
+ mockAgentPool = {
37
+ pauseAgent: jest.fn().mockResolvedValue({ success: true }),
38
+ getAgent: jest.fn().mockResolvedValue({ id: 'agent-1', delayEndTime: null }),
39
+ persistAgentState: jest.fn().mockResolvedValue(undefined)
40
+ };
41
+ tool = new AgentDelayTool({}, logger, mockAgentPool);
42
+ });
43
+
44
+ describe('constructor', () => {
45
+ test('should set default pause durations', () => {
46
+ expect(tool.maxPauseDuration).toBe(300);
47
+ expect(tool.minPauseDuration).toBe(1);
48
+ expect(tool.requiresProject).toBe(false);
49
+ expect(tool.isAsync).toBe(false);
50
+ });
51
+
52
+ test('should accept custom durations', () => {
53
+ const customTool = new AgentDelayTool({ maxDuration: 600, minDuration: 5 }, logger, null);
54
+ expect(customTool.maxPauseDuration).toBe(600);
55
+ expect(customTool.minPauseDuration).toBe(5);
56
+ });
57
+ });
58
+
59
+ describe('getDescription', () => {
60
+ test('should return description with pause range info', () => {
61
+ const desc = tool.getDescription();
62
+ expect(desc).toContain('Agent Delay Tool');
63
+ expect(desc).toContain('duration');
64
+ });
65
+ });
66
+
67
+ describe('getSupportedActions', () => {
68
+ test('should return pause and delay', () => {
69
+ const actions = tool.getSupportedActions();
70
+ expect(actions).toContain('pause');
71
+ expect(actions).toContain('delay');
72
+ });
73
+ });
74
+
75
+ describe('getRequiredParameters', () => {
76
+ test('should require duration', () => {
77
+ expect(tool.getRequiredParameters()).toContain('duration');
78
+ });
79
+ });
80
+
81
+ describe('parseParameters', () => {
82
+ test('should parse duration and reason from tags', () => {
83
+ const result = tool.parseParameters('<pause-duration>30</pause-duration><reason>Waiting</reason>');
84
+ expect(result.duration).toBe(30);
85
+ expect(result.reason).toBe('Waiting');
86
+ });
87
+
88
+ test('should default reason when not provided', () => {
89
+ const result = tool.parseParameters('<pause-duration>60</pause-duration>');
90
+ expect(result.duration).toBe(60);
91
+ expect(result.reason).toBe('Agent pause requested');
92
+ });
93
+
94
+ test('should return null duration when not provided', () => {
95
+ const result = tool.parseParameters('some content');
96
+ expect(result.duration).toBeNull();
97
+ });
98
+ });
99
+
100
+ describe('validateParameterTypes', () => {
101
+ test('should accept valid params', () => {
102
+ const result = tool.validateParameterTypes({ duration: 30, reason: 'test' });
103
+ expect(result.valid).toBe(true);
104
+ });
105
+
106
+ test('should reject non-number duration', () => {
107
+ const result = tool.validateParameterTypes({ duration: 'abc' });
108
+ expect(result.valid).toBe(false);
109
+ expect(result.errors[0]).toContain('valid number');
110
+ });
111
+
112
+ test('should reject non-string reason', () => {
113
+ const result = tool.validateParameterTypes({ duration: 30, reason: 123 });
114
+ expect(result.valid).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe('customValidateParameters', () => {
119
+ test('should reject null duration', () => {
120
+ const result = tool.customValidateParameters({ duration: null });
121
+ expect(result.valid).toBe(false);
122
+ expect(result.errors[0]).toContain('required');
123
+ });
124
+
125
+ test('should reject duration below minimum', () => {
126
+ const result = tool.customValidateParameters({ duration: 0 });
127
+ expect(result.valid).toBe(false);
128
+ expect(result.errors[0]).toContain('at least');
129
+ });
130
+
131
+ test('should reject duration above maximum', () => {
132
+ const result = tool.customValidateParameters({ duration: 999 });
133
+ expect(result.valid).toBe(false);
134
+ expect(result.errors[0]).toContain('cannot exceed');
135
+ });
136
+
137
+ test('should reject non-integer duration', () => {
138
+ const result = tool.customValidateParameters({ duration: 10.5 });
139
+ expect(result.valid).toBe(false);
140
+ expect(result.errors[0]).toContain('whole number');
141
+ });
142
+
143
+ test('should reject reason over 200 chars', () => {
144
+ const result = tool.customValidateParameters({ duration: 30, reason: 'x'.repeat(201) });
145
+ expect(result.valid).toBe(false);
146
+ });
147
+
148
+ test('should accept valid parameters', () => {
149
+ const result = tool.customValidateParameters({ duration: 60, reason: 'Valid reason' });
150
+ expect(result.valid).toBe(true);
151
+ });
152
+ });
153
+
154
+ describe('execute', () => {
155
+ test('should throw when agentId is missing', async () => {
156
+ await expect(tool.execute({ duration: 30, reason: 'test' }, {}))
157
+ .rejects.toThrow('Agent ID is required');
158
+ });
159
+
160
+ test('should throw when agentPool is not available', async () => {
161
+ tool.agentPool = null;
162
+ await expect(tool.execute({ duration: 30, reason: 'test' }, { agentId: 'agent-1' }))
163
+ .rejects.toThrow('Agent pool not available');
164
+ });
165
+
166
+ test('should successfully pause agent', async () => {
167
+ const result = await tool.execute(
168
+ { duration: 30, reason: 'npm install' },
169
+ { agentId: 'agent-1' }
170
+ );
171
+ expect(result.success).toBe(true);
172
+ expect(result.action).toBe('agent-pause');
173
+ expect(result.agentId).toBe('agent-1');
174
+ expect(result.pauseDuration).toBe(30);
175
+ expect(result.reason).toBe('npm install');
176
+ expect(result.pausedUntil).toBeDefined();
177
+ expect(mockAgentPool.pauseAgent).toHaveBeenCalled();
178
+ });
179
+
180
+ test('should set delayEndTime on agent', async () => {
181
+ const mockAgent = { id: 'agent-1', delayEndTime: null };
182
+ mockAgentPool.getAgent.mockResolvedValue(mockAgent);
183
+
184
+ await tool.execute(
185
+ { duration: 60, reason: 'build' },
186
+ { agentId: 'agent-1' }
187
+ );
188
+ expect(mockAgent.delayEndTime).toBeDefined();
189
+ expect(mockAgentPool.persistAgentState).toHaveBeenCalledWith('agent-1');
190
+ });
191
+
192
+ test('should throw when pause fails', async () => {
193
+ mockAgentPool.pauseAgent.mockResolvedValue({ success: false, message: 'Agent not found' });
194
+
195
+ await expect(tool.execute(
196
+ { duration: 30, reason: 'test' },
197
+ { agentId: 'agent-1' }
198
+ )).rejects.toThrow('Failed to pause agent');
199
+ });
200
+
201
+ test('should format singular second correctly', async () => {
202
+ const result = await tool.execute(
203
+ { duration: 1, reason: 'brief' },
204
+ { agentId: 'agent-1' }
205
+ );
206
+ expect(result.message).toBe('Agent will resume activity in 1 second');
207
+ });
208
+
209
+ test('should format plural seconds correctly', async () => {
210
+ const result = await tool.execute(
211
+ { duration: 30, reason: 'brief' },
212
+ { agentId: 'agent-1' }
213
+ );
214
+ expect(result.message).toBe('Agent will resume activity in 30 seconds');
215
+ });
216
+ });
217
+
218
+ describe('getCapabilities', () => {
219
+ test('should include pause range info', () => {
220
+ const caps = tool.getCapabilities();
221
+ expect(caps.pauseRange).toBeDefined();
222
+ expect(caps.pauseRange.min).toBe(1);
223
+ expect(caps.pauseRange.max).toBe(300);
224
+ expect(caps.affects).toBe('agent-status');
225
+ });
226
+ });
227
+
228
+ describe('formatResumeTime', () => {
229
+ test('should format seconds', () => {
230
+ const futureDate = new Date(Date.now() + 30000);
231
+ const result = tool.formatResumeTime(futureDate);
232
+ expect(result).toMatch(/in \d+ seconds?/);
233
+ });
234
+
235
+ test('should format minutes', () => {
236
+ const futureDate = new Date(Date.now() + 120000);
237
+ const result = tool.formatResumeTime(futureDate);
238
+ expect(result).toMatch(/in \d+ minutes?/);
239
+ });
240
+
241
+ test('should format hours', () => {
242
+ const futureDate = new Date(Date.now() + 7200000);
243
+ const result = tool.formatResumeTime(futureDate);
244
+ expect(result).toMatch(/in \d+ hours?/);
245
+ });
246
+ });
247
+
248
+ describe('canPauseAgent', () => {
249
+ test('should return false when no agent pool', async () => {
250
+ tool.agentPool = null;
251
+ const result = await tool.canPauseAgent('agent-1');
252
+ expect(result.canPause).toBe(false);
253
+ });
254
+
255
+ test('should return false when agent not found', async () => {
256
+ mockAgentPool.getAgent.mockResolvedValue(null);
257
+ const result = await tool.canPauseAgent('agent-1');
258
+ expect(result.canPause).toBe(false);
259
+ expect(result.reason).toContain('not found');
260
+ });
261
+
262
+ test('should return false when agent already paused', async () => {
263
+ mockAgentPool.getAgent.mockResolvedValue({ status: 'paused', pausedUntil: 'sometime' });
264
+ const result = await tool.canPauseAgent('agent-1');
265
+ expect(result.canPause).toBe(false);
266
+ expect(result.reason).toContain('already paused');
267
+ });
268
+
269
+ test('should return true when agent can be paused', async () => {
270
+ mockAgentPool.getAgent.mockResolvedValue({ status: 'active' });
271
+ const result = await tool.canPauseAgent('agent-1');
272
+ expect(result.canPause).toBe(true);
273
+ });
274
+
275
+ test('should handle errors gracefully', async () => {
276
+ mockAgentPool.getAgent.mockRejectedValue(new Error('DB error'));
277
+ const result = await tool.canPauseAgent('agent-1');
278
+ expect(result.canPause).toBe(false);
279
+ });
280
+ });
281
+
282
+ describe('getPauseRecommendations', () => {
283
+ test('should recommend for npm install', () => {
284
+ const result = tool.getPauseRecommendations({ lastCommand: 'npm install' });
285
+ expect(result.suggested.length).toBeGreaterThan(0);
286
+ expect(result.suggested[0].duration).toBe(90);
287
+ });
288
+
289
+ test('should recommend for docker build', () => {
290
+ const result = tool.getPauseRecommendations({ lastCommand: 'docker build .' });
291
+ expect(result.suggested.length).toBeGreaterThan(0);
292
+ });
293
+
294
+ test('should warn about short pauses', () => {
295
+ const result = tool.getPauseRecommendations({ requestedDuration: 5 });
296
+ expect(result.warnings.length).toBeGreaterThan(0);
297
+ });
298
+
299
+ test('should warn about long pauses', () => {
300
+ const result = tool.getPauseRecommendations({ requestedDuration: 250 });
301
+ expect(result.warnings.length).toBeGreaterThan(0);
302
+ });
303
+
304
+ test('should return empty for no context', () => {
305
+ const result = tool.getPauseRecommendations({});
306
+ expect(result.suggested).toEqual([]);
307
+ expect(result.warnings).toEqual([]);
308
+ });
309
+ });
310
+
311
+ describe('setAgentPool', () => {
312
+ test('should enable tool when pool is provided', () => {
313
+ const newTool = new AgentDelayTool({}, logger, null);
314
+ newTool.setAgentPool(mockAgentPool);
315
+ expect(newTool.isEnabled).toBe(true);
316
+ });
317
+
318
+ test('should disable tool when pool is null', () => {
319
+ tool.setAgentPool(null);
320
+ expect(tool.isEnabled).toBe(false);
321
+ });
322
+ });
323
+
324
+ describe('getUsageExamples', () => {
325
+ test('should return array of examples', () => {
326
+ const examples = tool.getUsageExamples();
327
+ expect(Array.isArray(examples)).toBe(true);
328
+ expect(examples.length).toBeGreaterThan(0);
329
+ expect(examples[0].title).toBeDefined();
330
+ expect(examples[0].command).toBeDefined();
331
+ });
332
+ });
333
+
334
+ describe('getParameterSchema', () => {
335
+ test('should return schema with duration and reason', () => {
336
+ const schema = tool.getParameterSchema();
337
+ expect(schema.properties.duration).toBeDefined();
338
+ expect(schema.properties.reason).toBeDefined();
339
+ expect(schema.required).toContain('duration');
340
+ });
341
+ });
342
+ });
@@ -0,0 +1,344 @@
1
+ import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
5
+ TOOL_STATUS: {
6
+ PENDING: 'pending',
7
+ EXECUTING: 'executing',
8
+ COMPLETED: 'completed',
9
+ FAILED: 'failed',
10
+ CANCELLED: 'cancelled',
11
+ TIMEOUT: 'timeout'
12
+ },
13
+ SYSTEM_DEFAULTS: { MAX_TOOL_EXECUTION_TIME: 300000 },
14
+ OPERATION_STATUS: {},
15
+ ERROR_TYPES: {}
16
+ }));
17
+
18
+ const { default: AsyncToolManager } = await import('../asyncToolManager.js');
19
+
20
+ describe('AsyncToolManager', () => {
21
+ let manager;
22
+ let logger;
23
+
24
+ beforeEach(() => {
25
+ jest.useFakeTimers();
26
+ logger = createMockLogger();
27
+ manager = new AsyncToolManager({
28
+ maxConcurrentOperations: 5,
29
+ defaultTimeout: 10000,
30
+ cleanupInterval: 60000
31
+ }, logger);
32
+ });
33
+
34
+ afterEach(() => {
35
+ manager.stopMonitoring();
36
+ manager.removeAllListeners();
37
+ jest.useRealTimers();
38
+ });
39
+
40
+ describe('constructor', () => {
41
+ test('should set default config', () => {
42
+ expect(manager.maxConcurrentOperations).toBe(5);
43
+ expect(manager.defaultTimeout).toBe(10000);
44
+ expect(manager.operations).toBeInstanceOf(Map);
45
+ expect(manager.operationHistory).toEqual([]);
46
+ });
47
+
48
+ test('should start monitoring on creation', () => {
49
+ expect(manager.monitoringInterval).not.toBeNull();
50
+ });
51
+ });
52
+
53
+ describe('startOperation', () => {
54
+ test('should create and track a new operation', async () => {
55
+ const opId = await manager.startOperation('filesystem', 'agent-1', { path: '/test' });
56
+ expect(typeof opId).toBe('string');
57
+ expect(opId).toMatch(/^op-/);
58
+ expect(manager.operations.has(opId)).toBe(true);
59
+ });
60
+
61
+ test('should set pending status initially', async () => {
62
+ const opId = await manager.startOperation('filesystem', 'agent-1', {});
63
+ const op = manager.operations.get(opId);
64
+ expect(op.status).toBe('pending');
65
+ });
66
+
67
+ test('should throw when max concurrent operations reached', async () => {
68
+ // Fill up operations
69
+ for (let i = 0; i < 5; i++) {
70
+ await manager.startOperation('tool', `agent-${i}`, {});
71
+ }
72
+ await expect(manager.startOperation('tool', 'agent-extra', {}))
73
+ .rejects.toThrow('Maximum concurrent operations');
74
+ });
75
+
76
+ test('should use custom timeout from context', async () => {
77
+ const opId = await manager.startOperation('tool', 'agent-1', {}, { timeout: 5000 });
78
+ const op = manager.operations.get(opId);
79
+ expect(op.timeout).toBe(5000);
80
+ });
81
+ });
82
+
83
+ describe('updateOperation', () => {
84
+ test('should update operation status', async () => {
85
+ const opId = await manager.startOperation('tool', 'agent-1', {});
86
+ const result = manager.updateOperation(opId, 'executing');
87
+ expect(result).toBe(true);
88
+ expect(manager.operations.get(opId).status).toBe('executing');
89
+ });
90
+
91
+ test('should set startedAt on executing', async () => {
92
+ const opId = await manager.startOperation('tool', 'agent-1', {});
93
+ manager.updateOperation(opId, 'executing');
94
+ expect(manager.operations.get(opId).startedAt).not.toBeNull();
95
+ });
96
+
97
+ test('should set completedAt on completed', async () => {
98
+ const opId = await manager.startOperation('tool', 'agent-1', {});
99
+ manager.updateOperation(opId, 'completed', { result: 'done' });
100
+ expect(manager.operations.get(opId).completedAt).not.toBeNull();
101
+ expect(manager.operations.get(opId).result).toBe('done');
102
+ });
103
+
104
+ test('should return false for unknown operation', () => {
105
+ const result = manager.updateOperation('nonexistent', 'completed');
106
+ expect(result).toBe(false);
107
+ });
108
+
109
+ test('should store error data', async () => {
110
+ const opId = await manager.startOperation('tool', 'agent-1', {});
111
+ manager.updateOperation(opId, 'failed', { error: 'Something broke' });
112
+ expect(manager.operations.get(opId).error).toBe('Something broke');
113
+ });
114
+
115
+ test('should store progress data', async () => {
116
+ const opId = await manager.startOperation('tool', 'agent-1', {});
117
+ manager.updateOperation(opId, 'executing', { progress: 50 });
118
+ expect(manager.operations.get(opId).progress).toBe(50);
119
+ });
120
+ });
121
+
122
+ describe('getOperation', () => {
123
+ test('should return operation details', async () => {
124
+ const opId = await manager.startOperation('filesystem', 'agent-1', { path: '/test' });
125
+ const op = manager.getOperation(opId);
126
+ expect(op).not.toBeNull();
127
+ expect(op.toolId).toBe('filesystem');
128
+ expect(op.agentId).toBe('agent-1');
129
+ expect(op.status).toBe('pending');
130
+ });
131
+
132
+ test('should return null for unknown operation', () => {
133
+ expect(manager.getOperation('nonexistent')).toBeNull();
134
+ });
135
+
136
+ test('should include executionTime', async () => {
137
+ const opId = await manager.startOperation('tool', 'agent-1', {});
138
+ manager.updateOperation(opId, 'executing');
139
+ const op = manager.getOperation(opId);
140
+ expect(op.executionTime).toBeDefined();
141
+ });
142
+ });
143
+
144
+ describe('getAgentOperations', () => {
145
+ test('should return only operations for specified agent', async () => {
146
+ await manager.startOperation('tool1', 'agent-1', {});
147
+ await manager.startOperation('tool2', 'agent-1', {});
148
+ await manager.startOperation('tool3', 'agent-2', {});
149
+
150
+ const ops = manager.getAgentOperations('agent-1');
151
+ expect(ops).toHaveLength(2);
152
+ });
153
+
154
+ test('returns all operations for agent', async () => {
155
+ const id1 = await manager.startOperation('tool1', 'agent-1', {});
156
+ const id2 = await manager.startOperation('tool2', 'agent-1', {});
157
+ const ops = manager.getAgentOperations('agent-1');
158
+ const ids = ops.map(op => op.id);
159
+ expect(ids).toContain(id1);
160
+ expect(ids).toContain(id2);
161
+ });
162
+ });
163
+
164
+ describe('cancelOperation', () => {
165
+ test('should cancel a pending operation', async () => {
166
+ const opId = await manager.startOperation('tool', 'agent-1', {});
167
+ const result = await manager.cancelOperation(opId, 'No longer needed');
168
+ expect(result).toBe(true);
169
+ });
170
+
171
+ test('should return false for unknown operation', async () => {
172
+ const result = await manager.cancelOperation('nonexistent');
173
+ expect(result).toBe(false);
174
+ });
175
+
176
+ test('should not cancel completed operation', async () => {
177
+ const opId = await manager.startOperation('tool', 'agent-1', {});
178
+ manager.updateOperation(opId, 'completed');
179
+ const result = await manager.cancelOperation(opId);
180
+ expect(result).toBe(false);
181
+ });
182
+ });
183
+
184
+ describe('retryOperation', () => {
185
+ test('should retry a failed operation with retries remaining', async () => {
186
+ const opId = await manager.startOperation('tool', 'agent-1', {}, { maxRetries: 3 });
187
+ manager.updateOperation(opId, 'failed', { error: 'timeout' });
188
+ const result = await manager.retryOperation(opId);
189
+ expect(result).toBe(true);
190
+ expect(manager.operations.get(opId).retryCount).toBe(1);
191
+ expect(manager.operations.get(opId).status).toBe('pending');
192
+ });
193
+
194
+ test('should not retry when max retries exceeded', async () => {
195
+ const opId = await manager.startOperation('tool', 'agent-1', {}, { maxRetries: 0 });
196
+ manager.updateOperation(opId, 'failed');
197
+ const result = await manager.retryOperation(opId);
198
+ expect(result).toBe(false);
199
+ });
200
+
201
+ test('should not retry non-failed operation', async () => {
202
+ const opId = await manager.startOperation('tool', 'agent-1', {});
203
+ const result = await manager.retryOperation(opId);
204
+ expect(result).toBe(false);
205
+ });
206
+
207
+ test('should return false for unknown operation', async () => {
208
+ const result = await manager.retryOperation('nonexistent');
209
+ expect(result).toBe(false);
210
+ });
211
+ });
212
+
213
+ describe('getStatistics', () => {
214
+ test('should return correct stats', async () => {
215
+ await manager.startOperation('filesystem', 'agent-1', {});
216
+ await manager.startOperation('terminal', 'agent-1', {});
217
+ await manager.startOperation('filesystem', 'agent-2', {});
218
+
219
+ const stats = manager.getStatistics();
220
+ expect(stats.total).toBe(3);
221
+ expect(stats.byTool.filesystem).toBe(2);
222
+ expect(stats.byTool.terminal).toBe(1);
223
+ expect(stats.byAgent['agent-1']).toBe(2);
224
+ });
225
+
226
+ test('should return empty stats when no operations', () => {
227
+ const stats = manager.getStatistics();
228
+ expect(stats.total).toBe(0);
229
+ expect(stats.averageExecutionTime).toBe(0);
230
+ });
231
+ });
232
+
233
+ describe('cleanupCompletedOperations', () => {
234
+ test('should clean up old completed operations', async () => {
235
+ const opId = await manager.startOperation('tool', 'agent-1', {});
236
+ manager.updateOperation(opId, 'completed');
237
+
238
+ // Set completedAt to past
239
+ manager.operations.get(opId).completedAt = new Date(Date.now() - 7200000).toISOString();
240
+
241
+ const count = await manager.cleanupCompletedOperations(3600000);
242
+ expect(count).toBe(1);
243
+ expect(manager.operations.has(opId)).toBe(false);
244
+ });
245
+
246
+ test('should not clean up recent operations', async () => {
247
+ const opId = await manager.startOperation('tool', 'agent-1', {});
248
+ manager.updateOperation(opId, 'completed');
249
+
250
+ const count = await manager.cleanupCompletedOperations(3600000);
251
+ expect(count).toBe(0);
252
+ });
253
+ });
254
+
255
+ describe('shutdown', () => {
256
+ test('should cancel active operations and stop monitoring', async () => {
257
+ await manager.startOperation('tool1', 'agent-1', {});
258
+ await manager.startOperation('tool2', 'agent-1', {});
259
+
260
+ await manager.shutdown();
261
+ expect(manager.isShuttingDown).toBe(true);
262
+ expect(manager.operations.size).toBe(0);
263
+ expect(manager.monitoringInterval).toBeNull();
264
+ });
265
+ });
266
+
267
+ describe('getOperationHistory', () => {
268
+ test('should return empty history initially', () => {
269
+ const history = manager.getOperationHistory();
270
+ expect(history).toEqual([]);
271
+ });
272
+
273
+ test('should filter by agentId', async () => {
274
+ // Add to history manually
275
+ manager.operationHistory.push(
276
+ { agentId: 'agent-1', toolId: 'tool1', createdAt: new Date().toISOString() },
277
+ { agentId: 'agent-2', toolId: 'tool2', createdAt: new Date().toISOString() }
278
+ );
279
+ const history = manager.getOperationHistory({ agentId: 'agent-1' });
280
+ expect(history).toHaveLength(1);
281
+ });
282
+
283
+ test('should filter by toolId', () => {
284
+ manager.operationHistory.push(
285
+ { agentId: 'agent-1', toolId: 'filesystem', createdAt: new Date().toISOString() },
286
+ { agentId: 'agent-1', toolId: 'terminal', createdAt: new Date().toISOString() }
287
+ );
288
+ const history = manager.getOperationHistory({ toolId: 'filesystem' });
289
+ expect(history).toHaveLength(1);
290
+ });
291
+
292
+ test('should filter by status', () => {
293
+ manager.operationHistory.push(
294
+ { agentId: 'agent-1', status: 'completed', createdAt: new Date().toISOString() },
295
+ { agentId: 'agent-1', status: 'failed', createdAt: new Date().toISOString() }
296
+ );
297
+ const history = manager.getOperationHistory({ status: 'completed' });
298
+ expect(history).toHaveLength(1);
299
+ });
300
+
301
+ test('should respect limit', () => {
302
+ for (let i = 0; i < 10; i++) {
303
+ manager.operationHistory.push({
304
+ agentId: 'agent-1', createdAt: new Date().toISOString()
305
+ });
306
+ }
307
+ const history = manager.getOperationHistory({ limit: 3 });
308
+ expect(history).toHaveLength(3);
309
+ });
310
+ });
311
+
312
+ describe('generateOperationId', () => {
313
+ test('should generate unique IDs', () => {
314
+ const id1 = manager.generateOperationId();
315
+ const id2 = manager.generateOperationId();
316
+ expect(id1).not.toBe(id2);
317
+ expect(id1).toMatch(/^op-/);
318
+ });
319
+ });
320
+
321
+ describe('calculateExecutionTime', () => {
322
+ test('should return 0 when not started', () => {
323
+ const result = manager.calculateExecutionTime({ startedAt: null });
324
+ expect(result).toBe(0);
325
+ });
326
+
327
+ test('should calculate time for completed operation', () => {
328
+ const start = new Date('2024-01-01T00:00:00Z');
329
+ const end = new Date('2024-01-01T00:01:00Z');
330
+ const result = manager.calculateExecutionTime({
331
+ startedAt: start.toISOString(),
332
+ completedAt: end.toISOString()
333
+ });
334
+ expect(result).toBe(60000);
335
+ });
336
+ });
337
+
338
+ describe('stopMonitoring', () => {
339
+ test('should clear the interval', () => {
340
+ manager.stopMonitoring();
341
+ expect(manager.monitoringInterval).toBeNull();
342
+ });
343
+ });
344
+ });