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,500 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock fs (promises) and crypto BEFORE importing the tool
5
+ const fsMock = {
6
+ mkdir: jest.fn().mockResolvedValue(undefined),
7
+ readFile: jest.fn().mockResolvedValue('{}'),
8
+ writeFile: jest.fn().mockResolvedValue(undefined),
9
+ stat: jest.fn().mockResolvedValue({ size: 100 }),
10
+ copyFile: jest.fn().mockResolvedValue(undefined),
11
+ access: jest.fn().mockResolvedValue(undefined)
12
+ };
13
+
14
+ jest.unstable_mockModule('fs', () => ({
15
+ default: { promises: fsMock },
16
+ promises: fsMock
17
+ }));
18
+
19
+ jest.unstable_mockModule('crypto', () => ({
20
+ default: {
21
+ randomUUID: jest.fn(() => 'mock-crypto-uuid'),
22
+ randomBytes: jest.fn(() => ({ toString: () => 'mockrandom' }))
23
+ }
24
+ }));
25
+
26
+ const { default: AgentCommunicationTool } = await import('../agentCommunicationTool.js');
27
+
28
+ /**
29
+ * Helper: builds a tool, agent pool, and context for tests.
30
+ */
31
+ function createTestSetup() {
32
+ const logger = createMockLogger();
33
+ const tool = new AgentCommunicationTool({ storageDir: '/tmp/msg-test' });
34
+ tool.logger = logger;
35
+
36
+ const senderAgent = {
37
+ id: 'agent-sender',
38
+ name: 'Sender Agent',
39
+ type: 'developer',
40
+ capabilities: ['code'],
41
+ status: 'active',
42
+ isPaused: false,
43
+ conversations: { full: { messages: [], lastUpdated: '' } },
44
+ currentModel: null
45
+ };
46
+
47
+ const recipientAgent = {
48
+ id: 'agent-recipient',
49
+ name: 'Recipient Agent',
50
+ type: 'reviewer',
51
+ capabilities: ['review'],
52
+ status: 'active',
53
+ isPaused: false,
54
+ conversations: { full: { messages: [], lastUpdated: '' } },
55
+ currentModel: null
56
+ };
57
+
58
+ const agents = new Map();
59
+ agents.set('agent-sender', senderAgent);
60
+ agents.set('agent-recipient', recipientAgent);
61
+
62
+ const agentPool = {
63
+ getAgent: jest.fn((id) => Promise.resolve(agents.get(id) || null)),
64
+ listActiveAgents: jest.fn().mockResolvedValue([senderAgent, recipientAgent]),
65
+ notifyAgent: jest.fn().mockResolvedValue(undefined),
66
+ persistAgentState: jest.fn().mockResolvedValue(undefined),
67
+ updateAgent: jest.fn().mockResolvedValue(undefined),
68
+ addInterAgentMessage: jest.fn().mockResolvedValue(undefined)
69
+ };
70
+
71
+ const context = {
72
+ agentId: 'agent-sender',
73
+ agentPool,
74
+ projectDir: '/tmp/test'
75
+ };
76
+
77
+ return { tool, senderAgent, recipientAgent, agentPool, context, logger };
78
+ }
79
+
80
+ describe('AgentCommunicationTool', () => {
81
+ // ── constructor ─────────────────────────────────────────────────
82
+ describe('constructor', () => {
83
+ test('initializes with default config values', () => {
84
+ const tool = new AgentCommunicationTool();
85
+ expect(tool.config.maxConversationDepth).toBe(10);
86
+ expect(tool.config.maxRecipientsPerMessage).toBe(3);
87
+ expect(tool.messages).toBeInstanceOf(Map);
88
+ expect(tool.conversations).toBeInstanceOf(Map);
89
+ });
90
+
91
+ test('accepts custom config overrides', () => {
92
+ const tool = new AgentCommunicationTool({ maxConversationDepth: 5 });
93
+ expect(tool.config.maxConversationDepth).toBe(5);
94
+ });
95
+ });
96
+
97
+ // ── getDescription ──────────────────────────────────────────────
98
+ describe('getDescription', () => {
99
+ test('returns description with action list', () => {
100
+ const tool = new AgentCommunicationTool();
101
+ const desc = tool.getDescription();
102
+ expect(desc).toContain('get-available-agents');
103
+ expect(desc).toContain('send-message');
104
+ expect(desc).toContain('reply-to-message');
105
+ });
106
+ });
107
+
108
+ // ── parseParameters ─────────────────────────────────────────────
109
+ describe('parseParameters', () => {
110
+ test('parses object with actions array', () => {
111
+ const tool = new AgentCommunicationTool();
112
+ const result = tool.parseParameters({
113
+ actions: [{ type: 'get-available-agents' }]
114
+ });
115
+ expect(result.action).toBe('get-available-agents');
116
+ });
117
+
118
+ test('parses object with action field', () => {
119
+ const tool = new AgentCommunicationTool();
120
+ const result = tool.parseParameters({ action: 'send-message', subject: 'Hi' });
121
+ expect(result.action).toBe('send-message');
122
+ });
123
+
124
+ test('parses JSON string', () => {
125
+ const tool = new AgentCommunicationTool();
126
+ const result = tool.parseParameters('{"action": "get-available-agents"}');
127
+ expect(result.action).toBe('get-available-agents');
128
+ });
129
+
130
+ test('parses XML-style content', () => {
131
+ const tool = new AgentCommunicationTool();
132
+ const result = tool.parseParameters('<action>send-message</action><subject>Hello</subject>');
133
+ expect(result.action).toBe('send-message');
134
+ expect(result.subject).toBe('Hello');
135
+ });
136
+
137
+ test('returns empty object for null input', () => {
138
+ const tool = new AgentCommunicationTool();
139
+ const result = tool.parseParameters(null);
140
+ expect(result).toEqual({});
141
+ });
142
+ });
143
+
144
+ // ── execute - missing action ────────────────────────────────────
145
+ describe('execute - missing params', () => {
146
+ test('throws when action is missing', async () => {
147
+ const { tool, context } = createTestSetup();
148
+ await expect(tool.execute({}, context)).rejects.toThrow('Action parameter is required');
149
+ });
150
+
151
+ test('throws when agentId is missing from context', async () => {
152
+ const { tool } = createTestSetup();
153
+ await expect(tool.execute({ action: 'get-available-agents' }, {}))
154
+ .rejects.toThrow('Agent ID is required');
155
+ });
156
+
157
+ test('throws for unknown action', async () => {
158
+ const { tool, context } = createTestSetup();
159
+ await expect(tool.execute({ action: 'teleport' }, context))
160
+ .rejects.toThrow('Unknown action');
161
+ });
162
+ });
163
+
164
+ // ── get-available-agents ────────────────────────────────────────
165
+ describe('execute - get-available-agents', () => {
166
+ test('returns list excluding requesting agent', async () => {
167
+ const { tool, context } = createTestSetup();
168
+ const result = await tool.execute({ action: 'get-available-agents' }, context);
169
+ expect(result.success).toBe(true);
170
+ expect(result.agents).toHaveLength(1);
171
+ expect(result.agents[0].id).toBe('agent-recipient');
172
+ });
173
+
174
+ test('includes message stats for each agent', async () => {
175
+ const { tool, context } = createTestSetup();
176
+ const result = await tool.execute({ action: 'get-available-agents' }, context);
177
+ expect(result.agents[0].messageStats).toBeDefined();
178
+ });
179
+
180
+ test('returns error when agentPool missing', async () => {
181
+ const { tool } = createTestSetup();
182
+ const result = await tool.execute(
183
+ { action: 'get-available-agents' },
184
+ { agentId: 'agent-sender', agentPool: null }
185
+ );
186
+ // Should catch internally and return success: false
187
+ expect(result.success).toBe(false);
188
+ expect(result.error).toBeDefined();
189
+ });
190
+ });
191
+
192
+ // ── send-message ────────────────────────────────────────────────
193
+ describe('execute - send-message', () => {
194
+ test('sends message to recipient successfully', async () => {
195
+ const { tool, context } = createTestSetup();
196
+ const result = await tool.execute({
197
+ action: 'send-message',
198
+ recipient: 'agent-recipient',
199
+ subject: 'Review code',
200
+ message: 'Please review the auth module'
201
+ }, context);
202
+ expect(result.success).toBe(true);
203
+ expect(result.messageId).toBeDefined();
204
+ expect(result.conversationId).toBeDefined();
205
+ expect(result.recipients).toContain('agent-recipient');
206
+ });
207
+
208
+ test('errors when subject is missing', async () => {
209
+ const { tool, context } = createTestSetup();
210
+ const result = await tool.execute({
211
+ action: 'send-message',
212
+ recipient: 'agent-recipient',
213
+ message: 'No subject'
214
+ }, context);
215
+ expect(result.success).toBe(false);
216
+ expect(result.error).toContain('Subject and message are required');
217
+ });
218
+
219
+ test('errors when message is missing', async () => {
220
+ const { tool, context } = createTestSetup();
221
+ const result = await tool.execute({
222
+ action: 'send-message',
223
+ recipient: 'agent-recipient',
224
+ subject: 'No body'
225
+ }, context);
226
+ expect(result.success).toBe(false);
227
+ });
228
+
229
+ test('errors when no recipient provided', async () => {
230
+ const { tool, context } = createTestSetup();
231
+ const result = await tool.execute({
232
+ action: 'send-message',
233
+ subject: 'Test',
234
+ message: 'Hello'
235
+ }, context);
236
+ expect(result.success).toBe(false);
237
+ expect(result.error).toContain('At least one recipient');
238
+ });
239
+
240
+ test('returns suggestions when recipient not found', async () => {
241
+ const { tool, context, agentPool } = createTestSetup();
242
+ // Mock _canSendMessage to allow (bypass delay check)
243
+ tool._canSendMessage = jest.fn().mockResolvedValue({ allowed: true });
244
+ const result = await tool.execute({
245
+ action: 'send-message',
246
+ recipient: 'agent-nonexistent',
247
+ subject: 'Test',
248
+ message: 'Hello'
249
+ }, context);
250
+ expect(result.success).toBe(false);
251
+ expect(result.suggestion).toBeDefined();
252
+ });
253
+
254
+ test('updates message counts after sending', async () => {
255
+ const { tool, context } = createTestSetup();
256
+ await tool.execute({
257
+ action: 'send-message',
258
+ recipient: 'agent-recipient',
259
+ subject: 'Test',
260
+ message: 'Content'
261
+ }, context);
262
+ const senderStats = tool.agentMessageCounts.get('agent-sender');
263
+ expect(senderStats.sent).toBe(1);
264
+ });
265
+ });
266
+
267
+ // ── reply-to-message ────────────────────────────────────────────
268
+ describe('execute - reply-to-message', () => {
269
+ test('replies to an existing message', async () => {
270
+ const { tool, context } = createTestSetup();
271
+ // Send initial message
272
+ const sendResult = await tool.execute({
273
+ action: 'send-message',
274
+ recipient: 'agent-recipient',
275
+ subject: 'Question',
276
+ message: 'What is the status?'
277
+ }, context);
278
+ const msgId = sendResult.messageId;
279
+
280
+ // Reply as recipient
281
+ const replyResult = await tool.execute({
282
+ action: 'reply-to-message',
283
+ 'message-id': msgId,
284
+ message: 'All good!'
285
+ }, { agentId: 'agent-recipient', agentPool: context.agentPool });
286
+ expect(replyResult.success).toBe(true);
287
+ expect(replyResult.depth).toBe(1);
288
+ });
289
+
290
+ test('errors when original message ID missing', async () => {
291
+ const { tool, context } = createTestSetup();
292
+ const result = await tool.execute({
293
+ action: 'reply-to-message',
294
+ message: 'Reply without ID'
295
+ }, context);
296
+ expect(result.success).toBe(false);
297
+ expect(result.error).toContain('Original message ID and reply content are required');
298
+ });
299
+
300
+ test('errors when original message not found', async () => {
301
+ const { tool, context } = createTestSetup();
302
+ const result = await tool.execute({
303
+ action: 'reply-to-message',
304
+ 'message-id': 'msg-nonexistent',
305
+ message: 'Reply'
306
+ }, context);
307
+ expect(result.success).toBe(false);
308
+ expect(result.error).toContain('Original message not found');
309
+ });
310
+
311
+ test('errors when sender is not a participant', async () => {
312
+ const { tool, context, agentPool } = createTestSetup();
313
+ // Send message between sender and recipient
314
+ const sendResult = await tool.execute({
315
+ action: 'send-message',
316
+ recipient: 'agent-recipient',
317
+ subject: 'Private',
318
+ message: 'Secret'
319
+ }, context);
320
+
321
+ // Try to reply as an outsider
322
+ const outsider = {
323
+ id: 'agent-outsider', name: 'Outsider', conversations: { full: { messages: [] } }
324
+ };
325
+ agentPool.getAgent.mockImplementation((id) => {
326
+ if (id === 'agent-outsider') return Promise.resolve(outsider);
327
+ if (id === 'agent-sender') return Promise.resolve(context.agentPool);
328
+ return Promise.resolve(null);
329
+ });
330
+
331
+ const result = await tool.execute({
332
+ action: 'reply-to-message',
333
+ 'message-id': sendResult.messageId,
334
+ message: 'Eavesdrop'
335
+ }, { agentId: 'agent-outsider', agentPool });
336
+ expect(result.success).toBe(false);
337
+ expect(result.error).toContain('not a participant');
338
+ });
339
+
340
+ test('rejects reply when conversation depth limit reached', async () => {
341
+ const { tool, context } = createTestSetup();
342
+ tool.config.maxConversationDepth = 1;
343
+
344
+ const sendResult = await tool.execute({
345
+ action: 'send-message',
346
+ recipient: 'agent-recipient',
347
+ subject: 'Deep',
348
+ message: 'Start'
349
+ }, context);
350
+
351
+ // First reply (depth 1) - should succeed
352
+ const reply1 = await tool.execute({
353
+ action: 'reply-to-message',
354
+ 'message-id': sendResult.messageId,
355
+ message: 'Reply 1'
356
+ }, { agentId: 'agent-recipient', agentPool: context.agentPool });
357
+ expect(reply1.success).toBe(true);
358
+
359
+ // Second reply (depth 2) should fail at depth limit 1
360
+ const reply2 = await tool.execute({
361
+ action: 'reply-to-message',
362
+ 'message-id': reply1.messageId,
363
+ message: 'Reply 2'
364
+ }, context);
365
+ expect(reply2.success).toBe(false);
366
+ expect(reply2.error).toContain('depth limit');
367
+ });
368
+ });
369
+
370
+ // ── get-unreplied-messages ──────────────────────────────────────
371
+ describe('execute - get-unreplied-messages', () => {
372
+ test('returns unreplied messages for agent', async () => {
373
+ const { tool, context } = createTestSetup();
374
+ // Send message requiring reply
375
+ await tool.execute({
376
+ action: 'send-message',
377
+ recipient: 'agent-recipient',
378
+ subject: 'Need reply',
379
+ message: 'Please respond',
380
+ 'requires-reply': true
381
+ }, context);
382
+
383
+ const result = await tool.execute(
384
+ { action: 'get-unreplied-messages' },
385
+ { agentId: 'agent-recipient', agentPool: context.agentPool }
386
+ );
387
+ expect(result.success).toBe(true);
388
+ expect(result.messages.length).toBeGreaterThan(0);
389
+ expect(result.messages[0].subject).toBe('Need reply');
390
+ });
391
+
392
+ test('returns empty list when no messages', async () => {
393
+ const { tool, context } = createTestSetup();
394
+ const result = await tool.execute(
395
+ { action: 'get-unreplied-messages' },
396
+ { agentId: 'agent-recipient', agentPool: context.agentPool }
397
+ );
398
+ expect(result.success).toBe(true);
399
+ expect(result.messages).toHaveLength(0);
400
+ });
401
+ });
402
+
403
+ // ── mark-conversation-ended ─────────────────────────────────────
404
+ describe('execute - mark-conversation-ended', () => {
405
+ test('ends a conversation successfully', async () => {
406
+ const { tool, context } = createTestSetup();
407
+ const sendResult = await tool.execute({
408
+ action: 'send-message',
409
+ recipient: 'agent-recipient',
410
+ subject: 'End me',
411
+ message: 'Done'
412
+ }, context);
413
+
414
+ const result = await tool.execute({
415
+ action: 'mark-conversation-ended',
416
+ 'conversation-id': sendResult.conversationId,
417
+ reason: 'Work complete'
418
+ }, context);
419
+ expect(result.success).toBe(true);
420
+ expect(result.status).toBe('ended');
421
+ });
422
+
423
+ test('errors when conversation-id is missing', async () => {
424
+ const { tool, context } = createTestSetup();
425
+ const result = await tool.execute(
426
+ { action: 'mark-conversation-ended' }, context
427
+ );
428
+ expect(result.success).toBe(false);
429
+ expect(result.error).toContain('Conversation ID is required');
430
+ });
431
+
432
+ test('errors for non-existent conversation', async () => {
433
+ const { tool, context } = createTestSetup();
434
+ const result = await tool.execute({
435
+ action: 'mark-conversation-ended',
436
+ 'conversation-id': 'conv-nope'
437
+ }, context);
438
+ expect(result.success).toBe(false);
439
+ expect(result.error).toContain('Conversation not found');
440
+ });
441
+
442
+ test('errors when agent is not a participant', async () => {
443
+ const { tool, context, agentPool } = createTestSetup();
444
+ const sendResult = await tool.execute({
445
+ action: 'send-message',
446
+ recipient: 'agent-recipient',
447
+ subject: 'Private',
448
+ message: 'Content'
449
+ }, context);
450
+
451
+ const result = await tool.execute({
452
+ action: 'mark-conversation-ended',
453
+ 'conversation-id': sendResult.conversationId
454
+ }, { agentId: 'agent-outsider', agentPool });
455
+ expect(result.success).toBe(false);
456
+ expect(result.error).toContain('not a participant');
457
+ });
458
+ });
459
+
460
+ // ── helper methods ──────────────────────────────────────────────
461
+ describe('_parseRecipients', () => {
462
+ test('parses single recipient string', () => {
463
+ const tool = new AgentCommunicationTool();
464
+ const result = tool._parseRecipients('agent-1', null);
465
+ expect(result).toEqual(['agent-1']);
466
+ });
467
+
468
+ test('parses JSON array of recipients', () => {
469
+ const tool = new AgentCommunicationTool();
470
+ const result = tool._parseRecipients(null, '["agent-1", "agent-2"]');
471
+ expect(result).toEqual(['agent-1', 'agent-2']);
472
+ });
473
+
474
+ test('handles array recipients directly', () => {
475
+ const tool = new AgentCommunicationTool();
476
+ const result = tool._parseRecipients(null, ['agent-1', 'agent-2']);
477
+ expect(result).toEqual(['agent-1', 'agent-2']);
478
+ });
479
+
480
+ test('deduplicates recipients', () => {
481
+ const tool = new AgentCommunicationTool();
482
+ const result = tool._parseRecipients('agent-1', ['agent-1', 'agent-2']);
483
+ expect(result).toEqual(['agent-1', 'agent-2']);
484
+ });
485
+
486
+ test('handles non-JSON string as single recipient', () => {
487
+ const tool = new AgentCommunicationTool();
488
+ const result = tool._parseRecipients(null, 'plain-id');
489
+ expect(result).toEqual(['plain-id']);
490
+ });
491
+ });
492
+
493
+ describe('_processAttachments', () => {
494
+ test('returns empty array for null/undefined', async () => {
495
+ const tool = new AgentCommunicationTool();
496
+ expect(await tool._processAttachments(null, 'agent-1')).toEqual([]);
497
+ expect(await tool._processAttachments(undefined, 'agent-1')).toEqual([]);
498
+ });
499
+ });
500
+ });