agenthub-multiagent-mcp 1.1.1 → 1.1.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.
@@ -1,517 +1,517 @@
1
- /**
2
- * Unit tests for MCP tool handlers
3
- */
4
-
5
- import { describe, it, expect, beforeEach, vi } from "vitest";
6
- import { registerTools, handleToolCall, ToolContext } from "./index.js";
7
- import { ApiClient } from "../client.js";
8
-
9
- // Mock the state module
10
- vi.mock("../state.js", () => ({
11
- loadState: vi.fn(() => null),
12
- saveState: vi.fn(),
13
- deleteState: vi.fn(),
14
- stateExists: vi.fn(() => false),
15
- getCurrentOwner: vi.fn(() => "test-user"),
16
- }));
17
-
18
- // Mock the ApiClient
19
- const mockClient = {
20
- registerAgent: vi.fn(),
21
- reconnectAgent: vi.fn(),
22
- startWork: vi.fn(),
23
- heartbeat: vi.fn(),
24
- disconnect: vi.fn(),
25
- getAgent: vi.fn(),
26
- listAgents: vi.fn(),
27
- sendMessage: vi.fn(),
28
- getInbox: vi.fn(),
29
- markRead: vi.fn(),
30
- reply: vi.fn(),
31
- listChannels: vi.fn(),
32
- joinChannel: vi.fn(),
33
- leaveChannel: vi.fn(),
34
- } as unknown as ApiClient;
35
-
36
- // Mock context
37
- function createMockContext(): ToolContext {
38
- let currentAgentId = "";
39
- return {
40
- getCurrentAgentId: () => currentAgentId,
41
- setCurrentAgentId: (id: string) => {
42
- currentAgentId = id;
43
- },
44
- stopHeartbeat: vi.fn(),
45
- getWorkingDir: () => "/tmp/test-workdir",
46
- };
47
- }
48
-
49
- describe("registerTools", () => {
50
- it("should return all 19 tools", () => {
51
- const tools = registerTools();
52
- expect(tools).toHaveLength(19);
53
- });
54
-
55
- it("should have required tool names", () => {
56
- const tools = registerTools();
57
- const names = tools.map((t) => t.name);
58
-
59
- expect(names).toContain("agent_register");
60
- expect(names).toContain("agent_start_work");
61
- expect(names).toContain("agent_set_status");
62
- expect(names).toContain("agent_disconnect");
63
- expect(names).toContain("send_message");
64
- expect(names).toContain("send_to_channel");
65
- expect(names).toContain("broadcast");
66
- expect(names).toContain("check_inbox");
67
- expect(names).toContain("mark_read");
68
- expect(names).toContain("reply");
69
- expect(names).toContain("list_agents");
70
- expect(names).toContain("get_agent");
71
- expect(names).toContain("list_channels");
72
- expect(names).toContain("join_channel");
73
- expect(names).toContain("leave_channel");
74
- });
75
-
76
- it("should have valid input schemas", () => {
77
- const tools = registerTools();
78
- for (const tool of tools) {
79
- expect(tool.inputSchema).toBeDefined();
80
- expect(tool.inputSchema.type).toBe("object");
81
- expect(tool.inputSchema.properties).toBeDefined();
82
- }
83
- });
84
- });
85
-
86
- describe("handleToolCall", () => {
87
- let context: ToolContext;
88
-
89
- beforeEach(() => {
90
- vi.clearAllMocks();
91
- context = createMockContext();
92
- // Default mock for inbox (used by message wrapping)
93
- vi.mocked(mockClient.getInbox).mockResolvedValue({ messages: [], total: 0 });
94
- });
95
-
96
- describe("agent_register", () => {
97
- it("should register agent and set current agent ID", async () => {
98
- const mockResult = {
99
- agent_id: "test-agent",
100
- token: "test-token-uuid",
101
- model: "claude-opus-4.5",
102
- model_provider: "anthropic",
103
- registered_at: "2024-01-01T00:00:00Z",
104
- slack_notified: false,
105
- pending_tasks_count: 0,
106
- unread_messages_count: 0,
107
- };
108
- vi.mocked(mockClient.registerAgent).mockResolvedValue(mockResult);
109
-
110
- const result = await handleToolCall(
111
- "agent_register",
112
- { id: "test-agent", name: "Test Agent", model: "claude-opus-4.5" },
113
- mockClient,
114
- context
115
- );
116
-
117
- expect(mockClient.registerAgent).toHaveBeenCalledWith(
118
- "test-agent",
119
- "Test Agent",
120
- "test-user",
121
- "/tmp/test-workdir",
122
- "claude-opus-4.5"
123
- );
124
- expect(context.getCurrentAgentId()).toBe("test-agent");
125
- // Result is extended with mode and message
126
- expect(result).toMatchObject({
127
- ...mockResult,
128
- mode: "registered",
129
- });
130
- });
131
- });
132
-
133
- describe("agent_start_work", () => {
134
- it("should throw if not registered", async () => {
135
- await expect(
136
- handleToolCall(
137
- "agent_start_work",
138
- { task: "Building something" },
139
- mockClient,
140
- context
141
- )
142
- ).rejects.toThrow("Not registered");
143
- });
144
-
145
- it("should start work when registered", async () => {
146
- context.setCurrentAgentId("test-agent");
147
- const mockResult = { acknowledged: true, slack_posted: true };
148
- vi.mocked(mockClient.startWork).mockResolvedValue(mockResult);
149
-
150
- const result = await handleToolCall(
151
- "agent_start_work",
152
- { task: "Building feature X", project: "TestProject" },
153
- mockClient,
154
- context
155
- );
156
-
157
- expect(mockClient.startWork).toHaveBeenCalledWith(
158
- "test-agent",
159
- "Building feature X",
160
- "TestProject"
161
- );
162
- // Result is wrapped with messages
163
- expect(result).toMatchObject({ result: mockResult });
164
- });
165
- });
166
-
167
- describe("agent_set_status", () => {
168
- it("should throw if not registered", async () => {
169
- await expect(
170
- handleToolCall(
171
- "agent_set_status",
172
- { status: "busy" },
173
- mockClient,
174
- context
175
- )
176
- ).rejects.toThrow("Not registered");
177
- });
178
-
179
- it("should update status when registered", async () => {
180
- context.setCurrentAgentId("test-agent");
181
- const mockResult = { acknowledged: true, timestamp: "2024-01-01T00:00:00Z" };
182
- vi.mocked(mockClient.heartbeat).mockResolvedValue(mockResult);
183
-
184
- const result = await handleToolCall(
185
- "agent_set_status",
186
- { status: "busy" },
187
- mockClient,
188
- context
189
- );
190
-
191
- expect(mockClient.heartbeat).toHaveBeenCalledWith("test-agent", "busy");
192
- // Result is wrapped with messages
193
- expect(result).toMatchObject({ result: mockResult });
194
- });
195
- });
196
-
197
- describe("agent_disconnect", () => {
198
- it("should throw if not registered", async () => {
199
- await expect(
200
- handleToolCall("agent_disconnect", {}, mockClient, context)
201
- ).rejects.toThrow("Not registered");
202
- });
203
-
204
- it("should disconnect and stop heartbeat when registered", async () => {
205
- context.setCurrentAgentId("test-agent");
206
- const mockResult = { unregistered: true };
207
- vi.mocked(mockClient.disconnect).mockResolvedValue(mockResult);
208
-
209
- const result = await handleToolCall(
210
- "agent_disconnect",
211
- {},
212
- mockClient,
213
- context
214
- );
215
-
216
- expect(context.stopHeartbeat).toHaveBeenCalled();
217
- expect(mockClient.disconnect).toHaveBeenCalledWith("test-agent");
218
- expect(result).toEqual(mockResult);
219
- });
220
- });
221
-
222
- describe("send_message", () => {
223
- it("should throw if not registered", async () => {
224
- await expect(
225
- handleToolCall(
226
- "send_message",
227
- { to: "other-agent", type: "task", subject: "Test", body: "Content" },
228
- mockClient,
229
- context
230
- )
231
- ).rejects.toThrow("Not registered");
232
- });
233
-
234
- it("should send message when registered", async () => {
235
- context.setCurrentAgentId("test-agent");
236
- const mockResult = {
237
- message_id: "msg-1",
238
- status: "pending",
239
- created_at: "2024-01-01T00:00:00Z",
240
- };
241
- vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
242
-
243
- const result = await handleToolCall(
244
- "send_message",
245
- {
246
- to: "other-agent",
247
- type: "task",
248
- subject: "Test Task",
249
- body: "Please do this",
250
- priority: "high",
251
- },
252
- mockClient,
253
- context
254
- );
255
-
256
- expect(mockClient.sendMessage).toHaveBeenCalledWith({
257
- from_agent: "test-agent",
258
- to_agent: "other-agent",
259
- type: "task",
260
- subject: "Test Task",
261
- body: "Please do this",
262
- priority: "high",
263
- });
264
- // Result is wrapped with messages
265
- expect(result).toMatchObject({ result: mockResult });
266
- });
267
- });
268
-
269
- describe("send_to_channel", () => {
270
- it("should send message to channel", async () => {
271
- context.setCurrentAgentId("test-agent");
272
- const mockResult = {
273
- message_id: "msg-1",
274
- status: "pending",
275
- created_at: "2024-01-01T00:00:00Z",
276
- };
277
- vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
278
-
279
- const result = await handleToolCall(
280
- "send_to_channel",
281
- {
282
- channel: "team-channel",
283
- type: "status",
284
- subject: "Update",
285
- body: "Everything is good",
286
- },
287
- mockClient,
288
- context
289
- );
290
-
291
- expect(mockClient.sendMessage).toHaveBeenCalledWith({
292
- from_agent: "test-agent",
293
- to_channel: "team-channel",
294
- type: "status",
295
- subject: "Update",
296
- body: "Everything is good",
297
- });
298
- // Result is wrapped with messages
299
- expect(result).toMatchObject({ result: mockResult });
300
- });
301
- });
302
-
303
- describe("broadcast", () => {
304
- it("should send broadcast message", async () => {
305
- context.setCurrentAgentId("test-agent");
306
- const mockResult = {
307
- message_id: "msg-1",
308
- status: "pending",
309
- created_at: "2024-01-01T00:00:00Z",
310
- };
311
- vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
312
-
313
- const result = await handleToolCall(
314
- "broadcast",
315
- {
316
- type: "status",
317
- subject: "System Alert",
318
- body: "All systems operational",
319
- },
320
- mockClient,
321
- context
322
- );
323
-
324
- expect(mockClient.sendMessage).toHaveBeenCalledWith({
325
- from_agent: "test-agent",
326
- broadcast: true,
327
- type: "status",
328
- subject: "System Alert",
329
- body: "All systems operational",
330
- });
331
- // Result is wrapped with messages
332
- expect(result).toMatchObject({ result: mockResult });
333
- });
334
- });
335
-
336
- describe("check_inbox", () => {
337
- it("should get inbox messages", async () => {
338
- context.setCurrentAgentId("test-agent");
339
- const mockResult = { messages: [], total: 0 };
340
- vi.mocked(mockClient.getInbox).mockResolvedValue(mockResult);
341
-
342
- const result = await handleToolCall(
343
- "check_inbox",
344
- { unread_only: true },
345
- mockClient,
346
- context
347
- );
348
-
349
- expect(mockClient.getInbox).toHaveBeenCalledWith("test-agent", true);
350
- expect(result).toEqual(mockResult);
351
- });
352
- });
353
-
354
- describe("mark_read", () => {
355
- it("should mark message as read", async () => {
356
- // When not registered, result is NOT wrapped
357
- const mockResult = { updated: true };
358
- vi.mocked(mockClient.markRead).mockResolvedValue(mockResult);
359
-
360
- const result = await handleToolCall(
361
- "mark_read",
362
- { message_id: "msg-1" },
363
- mockClient,
364
- context
365
- );
366
-
367
- expect(mockClient.markRead).toHaveBeenCalledWith("msg-1");
368
- expect(result).toEqual(mockResult);
369
- });
370
- });
371
-
372
- describe("list_agents", () => {
373
- it("should list agents without filter", async () => {
374
- // When not registered, result is NOT wrapped
375
- const mockResult = { agents: [], total: 0 };
376
- vi.mocked(mockClient.listAgents).mockResolvedValue(mockResult);
377
-
378
- const result = await handleToolCall("list_agents", {}, mockClient, context);
379
-
380
- expect(mockClient.listAgents).toHaveBeenCalledWith(undefined);
381
- expect(result).toEqual(mockResult);
382
- });
383
-
384
- it("should list agents with status filter", async () => {
385
- // When not registered, result is NOT wrapped
386
- const mockResult = {
387
- agents: [
388
- {
389
- id: "agent-1",
390
- name: "Agent 1",
391
- owner: "test-user",
392
- model: "claude-opus-4.5",
393
- model_provider: "anthropic",
394
- status: "online" as const,
395
- working_dir: "",
396
- current_task: "",
397
- channels: [],
398
- registered_at: "",
399
- last_heartbeat: "",
400
- },
401
- ],
402
- total: 1,
403
- };
404
- vi.mocked(mockClient.listAgents).mockResolvedValue(mockResult);
405
-
406
- const result = await handleToolCall(
407
- "list_agents",
408
- { status: "online" },
409
- mockClient,
410
- context
411
- );
412
-
413
- expect(mockClient.listAgents).toHaveBeenCalledWith("online");
414
- expect(result).toEqual(mockResult);
415
- });
416
- });
417
-
418
- describe("get_agent", () => {
419
- it("should get agent details", async () => {
420
- // When not registered, result is NOT wrapped
421
- const mockAgent = {
422
- id: "agent-1",
423
- name: "Agent 1",
424
- owner: "test-user",
425
- model: "claude-opus-4.5",
426
- model_provider: "anthropic",
427
- status: "online" as const,
428
- working_dir: "/path",
429
- current_task: "Task",
430
- channels: [],
431
- registered_at: "",
432
- last_heartbeat: "",
433
- };
434
- vi.mocked(mockClient.getAgent).mockResolvedValue(mockAgent);
435
-
436
- const result = await handleToolCall(
437
- "get_agent",
438
- { id: "agent-1" },
439
- mockClient,
440
- context
441
- );
442
-
443
- expect(mockClient.getAgent).toHaveBeenCalledWith("agent-1");
444
- expect(result).toEqual(mockAgent);
445
- });
446
- });
447
-
448
- describe("list_channels", () => {
449
- it("should list channels", async () => {
450
- // When not registered, result is NOT wrapped
451
- const mockResult = { channels: [], total: 0 };
452
- vi.mocked(mockClient.listChannels).mockResolvedValue(mockResult);
453
-
454
- const result = await handleToolCall(
455
- "list_channels",
456
- {},
457
- mockClient,
458
- context
459
- );
460
-
461
- expect(mockClient.listChannels).toHaveBeenCalled();
462
- expect(result).toEqual(mockResult);
463
- });
464
- });
465
-
466
- describe("join_channel", () => {
467
- it("should join channel", async () => {
468
- context.setCurrentAgentId("test-agent");
469
- const mockResult = { subscribed: true };
470
- vi.mocked(mockClient.joinChannel).mockResolvedValue(mockResult);
471
-
472
- const result = await handleToolCall(
473
- "join_channel",
474
- { channel: "team-channel" },
475
- mockClient,
476
- context
477
- );
478
-
479
- expect(mockClient.joinChannel).toHaveBeenCalledWith(
480
- "team-channel",
481
- "test-agent"
482
- );
483
- // Result is wrapped with messages
484
- expect(result).toMatchObject({ result: mockResult });
485
- });
486
- });
487
-
488
- describe("leave_channel", () => {
489
- it("should leave channel", async () => {
490
- context.setCurrentAgentId("test-agent");
491
- const mockResult = { unsubscribed: true };
492
- vi.mocked(mockClient.leaveChannel).mockResolvedValue(mockResult);
493
-
494
- const result = await handleToolCall(
495
- "leave_channel",
496
- { channel: "team-channel" },
497
- mockClient,
498
- context
499
- );
500
-
501
- expect(mockClient.leaveChannel).toHaveBeenCalledWith(
502
- "team-channel",
503
- "test-agent"
504
- );
505
- // Result is wrapped with messages
506
- expect(result).toMatchObject({ result: mockResult });
507
- });
508
- });
509
-
510
- describe("unknown tool", () => {
511
- it("should throw for unknown tool", async () => {
512
- await expect(
513
- handleToolCall("unknown_tool", {}, mockClient, context)
514
- ).rejects.toThrow("Unknown tool: unknown_tool");
515
- });
516
- });
517
- });
1
+ /**
2
+ * Unit tests for MCP tool handlers
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
6
+ import { registerTools, handleToolCall, ToolContext } from "./index.js";
7
+ import { ApiClient } from "../client.js";
8
+
9
+ // Mock the state module
10
+ vi.mock("../state.js", () => ({
11
+ loadState: vi.fn(() => null),
12
+ saveState: vi.fn(),
13
+ deleteState: vi.fn(),
14
+ stateExists: vi.fn(() => false),
15
+ getCurrentOwner: vi.fn(() => "test-user"),
16
+ }));
17
+
18
+ // Mock the ApiClient
19
+ const mockClient = {
20
+ registerAgent: vi.fn(),
21
+ reconnectAgent: vi.fn(),
22
+ startWork: vi.fn(),
23
+ heartbeat: vi.fn(),
24
+ disconnect: vi.fn(),
25
+ getAgent: vi.fn(),
26
+ listAgents: vi.fn(),
27
+ sendMessage: vi.fn(),
28
+ getInbox: vi.fn(),
29
+ markRead: vi.fn(),
30
+ reply: vi.fn(),
31
+ listChannels: vi.fn(),
32
+ joinChannel: vi.fn(),
33
+ leaveChannel: vi.fn(),
34
+ } as unknown as ApiClient;
35
+
36
+ // Mock context
37
+ function createMockContext(): ToolContext {
38
+ let currentAgentId = "";
39
+ return {
40
+ getCurrentAgentId: () => currentAgentId,
41
+ setCurrentAgentId: (id: string) => {
42
+ currentAgentId = id;
43
+ },
44
+ stopHeartbeat: vi.fn(),
45
+ getWorkingDir: () => "/tmp/test-workdir",
46
+ };
47
+ }
48
+
49
+ describe("registerTools", () => {
50
+ it("should return all 19 tools", () => {
51
+ const tools = registerTools();
52
+ expect(tools).toHaveLength(19);
53
+ });
54
+
55
+ it("should have required tool names", () => {
56
+ const tools = registerTools();
57
+ const names = tools.map((t) => t.name);
58
+
59
+ expect(names).toContain("agent_register");
60
+ expect(names).toContain("agent_start_work");
61
+ expect(names).toContain("agent_set_status");
62
+ expect(names).toContain("agent_disconnect");
63
+ expect(names).toContain("send_message");
64
+ expect(names).toContain("send_to_channel");
65
+ expect(names).toContain("broadcast");
66
+ expect(names).toContain("check_inbox");
67
+ expect(names).toContain("mark_read");
68
+ expect(names).toContain("reply");
69
+ expect(names).toContain("list_agents");
70
+ expect(names).toContain("get_agent");
71
+ expect(names).toContain("list_channels");
72
+ expect(names).toContain("join_channel");
73
+ expect(names).toContain("leave_channel");
74
+ });
75
+
76
+ it("should have valid input schemas", () => {
77
+ const tools = registerTools();
78
+ for (const tool of tools) {
79
+ expect(tool.inputSchema).toBeDefined();
80
+ expect(tool.inputSchema.type).toBe("object");
81
+ expect(tool.inputSchema.properties).toBeDefined();
82
+ }
83
+ });
84
+ });
85
+
86
+ describe("handleToolCall", () => {
87
+ let context: ToolContext;
88
+
89
+ beforeEach(() => {
90
+ vi.clearAllMocks();
91
+ context = createMockContext();
92
+ // Default mock for inbox (used by message wrapping)
93
+ vi.mocked(mockClient.getInbox).mockResolvedValue({ messages: [], total: 0 });
94
+ });
95
+
96
+ describe("agent_register", () => {
97
+ it("should register agent and set current agent ID", async () => {
98
+ const mockResult = {
99
+ agent_id: "test-agent",
100
+ token: "test-token-uuid",
101
+ model: "claude-opus-4.5",
102
+ model_provider: "anthropic",
103
+ registered_at: "2024-01-01T00:00:00Z",
104
+ slack_notified: false,
105
+ pending_tasks_count: 0,
106
+ unread_messages_count: 0,
107
+ };
108
+ vi.mocked(mockClient.registerAgent).mockResolvedValue(mockResult);
109
+
110
+ const result = await handleToolCall(
111
+ "agent_register",
112
+ { id: "test-agent", name: "Test Agent", model: "claude-opus-4.5" },
113
+ mockClient,
114
+ context
115
+ );
116
+
117
+ expect(mockClient.registerAgent).toHaveBeenCalledWith(
118
+ "test-agent",
119
+ "Test Agent",
120
+ "test-user",
121
+ "/tmp/test-workdir",
122
+ "claude-opus-4.5"
123
+ );
124
+ expect(context.getCurrentAgentId()).toBe("test-agent");
125
+ // Result is extended with mode and message
126
+ expect(result).toMatchObject({
127
+ ...mockResult,
128
+ mode: "registered",
129
+ });
130
+ });
131
+ });
132
+
133
+ describe("agent_start_work", () => {
134
+ it("should throw if not registered", async () => {
135
+ await expect(
136
+ handleToolCall(
137
+ "agent_start_work",
138
+ { task: "Building something" },
139
+ mockClient,
140
+ context
141
+ )
142
+ ).rejects.toThrow("Not registered");
143
+ });
144
+
145
+ it("should start work when registered", async () => {
146
+ context.setCurrentAgentId("test-agent");
147
+ const mockResult = { acknowledged: true, slack_posted: true };
148
+ vi.mocked(mockClient.startWork).mockResolvedValue(mockResult);
149
+
150
+ const result = await handleToolCall(
151
+ "agent_start_work",
152
+ { task: "Building feature X", project: "TestProject" },
153
+ mockClient,
154
+ context
155
+ );
156
+
157
+ expect(mockClient.startWork).toHaveBeenCalledWith(
158
+ "test-agent",
159
+ "Building feature X",
160
+ "TestProject"
161
+ );
162
+ // Result is wrapped with messages
163
+ expect(result).toMatchObject({ result: mockResult });
164
+ });
165
+ });
166
+
167
+ describe("agent_set_status", () => {
168
+ it("should throw if not registered", async () => {
169
+ await expect(
170
+ handleToolCall(
171
+ "agent_set_status",
172
+ { status: "busy" },
173
+ mockClient,
174
+ context
175
+ )
176
+ ).rejects.toThrow("Not registered");
177
+ });
178
+
179
+ it("should update status when registered", async () => {
180
+ context.setCurrentAgentId("test-agent");
181
+ const mockResult = { acknowledged: true, timestamp: "2024-01-01T00:00:00Z" };
182
+ vi.mocked(mockClient.heartbeat).mockResolvedValue(mockResult);
183
+
184
+ const result = await handleToolCall(
185
+ "agent_set_status",
186
+ { status: "busy" },
187
+ mockClient,
188
+ context
189
+ );
190
+
191
+ expect(mockClient.heartbeat).toHaveBeenCalledWith("test-agent", "busy");
192
+ // Result is wrapped with messages
193
+ expect(result).toMatchObject({ result: mockResult });
194
+ });
195
+ });
196
+
197
+ describe("agent_disconnect", () => {
198
+ it("should throw if not registered", async () => {
199
+ await expect(
200
+ handleToolCall("agent_disconnect", {}, mockClient, context)
201
+ ).rejects.toThrow("Not registered");
202
+ });
203
+
204
+ it("should disconnect and stop heartbeat when registered", async () => {
205
+ context.setCurrentAgentId("test-agent");
206
+ const mockResult = { unregistered: true };
207
+ vi.mocked(mockClient.disconnect).mockResolvedValue(mockResult);
208
+
209
+ const result = await handleToolCall(
210
+ "agent_disconnect",
211
+ {},
212
+ mockClient,
213
+ context
214
+ );
215
+
216
+ expect(context.stopHeartbeat).toHaveBeenCalled();
217
+ expect(mockClient.disconnect).toHaveBeenCalledWith("test-agent");
218
+ expect(result).toEqual(mockResult);
219
+ });
220
+ });
221
+
222
+ describe("send_message", () => {
223
+ it("should throw if not registered", async () => {
224
+ await expect(
225
+ handleToolCall(
226
+ "send_message",
227
+ { to: "other-agent", type: "task", subject: "Test", body: "Content" },
228
+ mockClient,
229
+ context
230
+ )
231
+ ).rejects.toThrow("Not registered");
232
+ });
233
+
234
+ it("should send message when registered", async () => {
235
+ context.setCurrentAgentId("test-agent");
236
+ const mockResult = {
237
+ message_id: "msg-1",
238
+ status: "pending",
239
+ created_at: "2024-01-01T00:00:00Z",
240
+ };
241
+ vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
242
+
243
+ const result = await handleToolCall(
244
+ "send_message",
245
+ {
246
+ to: "other-agent",
247
+ type: "task",
248
+ subject: "Test Task",
249
+ body: "Please do this",
250
+ priority: "high",
251
+ },
252
+ mockClient,
253
+ context
254
+ );
255
+
256
+ expect(mockClient.sendMessage).toHaveBeenCalledWith({
257
+ from_agent: "test-agent",
258
+ to_agent: "other-agent",
259
+ type: "task",
260
+ subject: "Test Task",
261
+ body: "Please do this",
262
+ priority: "high",
263
+ });
264
+ // Result is wrapped with messages
265
+ expect(result).toMatchObject({ result: mockResult });
266
+ });
267
+ });
268
+
269
+ describe("send_to_channel", () => {
270
+ it("should send message to channel", async () => {
271
+ context.setCurrentAgentId("test-agent");
272
+ const mockResult = {
273
+ message_id: "msg-1",
274
+ status: "pending",
275
+ created_at: "2024-01-01T00:00:00Z",
276
+ };
277
+ vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
278
+
279
+ const result = await handleToolCall(
280
+ "send_to_channel",
281
+ {
282
+ channel: "team-channel",
283
+ type: "status",
284
+ subject: "Update",
285
+ body: "Everything is good",
286
+ },
287
+ mockClient,
288
+ context
289
+ );
290
+
291
+ expect(mockClient.sendMessage).toHaveBeenCalledWith({
292
+ from_agent: "test-agent",
293
+ to_channel: "team-channel",
294
+ type: "status",
295
+ subject: "Update",
296
+ body: "Everything is good",
297
+ });
298
+ // Result is wrapped with messages
299
+ expect(result).toMatchObject({ result: mockResult });
300
+ });
301
+ });
302
+
303
+ describe("broadcast", () => {
304
+ it("should send broadcast message", async () => {
305
+ context.setCurrentAgentId("test-agent");
306
+ const mockResult = {
307
+ message_id: "msg-1",
308
+ status: "pending",
309
+ created_at: "2024-01-01T00:00:00Z",
310
+ };
311
+ vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
312
+
313
+ const result = await handleToolCall(
314
+ "broadcast",
315
+ {
316
+ type: "status",
317
+ subject: "System Alert",
318
+ body: "All systems operational",
319
+ },
320
+ mockClient,
321
+ context
322
+ );
323
+
324
+ expect(mockClient.sendMessage).toHaveBeenCalledWith({
325
+ from_agent: "test-agent",
326
+ broadcast: true,
327
+ type: "status",
328
+ subject: "System Alert",
329
+ body: "All systems operational",
330
+ });
331
+ // Result is wrapped with messages
332
+ expect(result).toMatchObject({ result: mockResult });
333
+ });
334
+ });
335
+
336
+ describe("check_inbox", () => {
337
+ it("should get inbox messages", async () => {
338
+ context.setCurrentAgentId("test-agent");
339
+ const mockResult = { messages: [], total: 0 };
340
+ vi.mocked(mockClient.getInbox).mockResolvedValue(mockResult);
341
+
342
+ const result = await handleToolCall(
343
+ "check_inbox",
344
+ { unread_only: true },
345
+ mockClient,
346
+ context
347
+ );
348
+
349
+ expect(mockClient.getInbox).toHaveBeenCalledWith("test-agent", true);
350
+ expect(result).toEqual(mockResult);
351
+ });
352
+ });
353
+
354
+ describe("mark_read", () => {
355
+ it("should mark message as read", async () => {
356
+ // When not registered, result is NOT wrapped
357
+ const mockResult = { updated: true };
358
+ vi.mocked(mockClient.markRead).mockResolvedValue(mockResult);
359
+
360
+ const result = await handleToolCall(
361
+ "mark_read",
362
+ { message_id: "msg-1" },
363
+ mockClient,
364
+ context
365
+ );
366
+
367
+ expect(mockClient.markRead).toHaveBeenCalledWith("msg-1");
368
+ expect(result).toEqual(mockResult);
369
+ });
370
+ });
371
+
372
+ describe("list_agents", () => {
373
+ it("should list agents without filter", async () => {
374
+ // When not registered, result is NOT wrapped
375
+ const mockResult = { agents: [], total: 0 };
376
+ vi.mocked(mockClient.listAgents).mockResolvedValue(mockResult);
377
+
378
+ const result = await handleToolCall("list_agents", {}, mockClient, context);
379
+
380
+ expect(mockClient.listAgents).toHaveBeenCalledWith(undefined);
381
+ expect(result).toEqual(mockResult);
382
+ });
383
+
384
+ it("should list agents with status filter", async () => {
385
+ // When not registered, result is NOT wrapped
386
+ const mockResult = {
387
+ agents: [
388
+ {
389
+ id: "agent-1",
390
+ name: "Agent 1",
391
+ owner: "test-user",
392
+ model: "claude-opus-4.5",
393
+ model_provider: "anthropic",
394
+ status: "online" as const,
395
+ working_dir: "",
396
+ current_task: "",
397
+ channels: [],
398
+ registered_at: "",
399
+ last_heartbeat: "",
400
+ },
401
+ ],
402
+ total: 1,
403
+ };
404
+ vi.mocked(mockClient.listAgents).mockResolvedValue(mockResult);
405
+
406
+ const result = await handleToolCall(
407
+ "list_agents",
408
+ { status: "online" },
409
+ mockClient,
410
+ context
411
+ );
412
+
413
+ expect(mockClient.listAgents).toHaveBeenCalledWith("online");
414
+ expect(result).toEqual(mockResult);
415
+ });
416
+ });
417
+
418
+ describe("get_agent", () => {
419
+ it("should get agent details", async () => {
420
+ // When not registered, result is NOT wrapped
421
+ const mockAgent = {
422
+ id: "agent-1",
423
+ name: "Agent 1",
424
+ owner: "test-user",
425
+ model: "claude-opus-4.5",
426
+ model_provider: "anthropic",
427
+ status: "online" as const,
428
+ working_dir: "/path",
429
+ current_task: "Task",
430
+ channels: [],
431
+ registered_at: "",
432
+ last_heartbeat: "",
433
+ };
434
+ vi.mocked(mockClient.getAgent).mockResolvedValue(mockAgent);
435
+
436
+ const result = await handleToolCall(
437
+ "get_agent",
438
+ { id: "agent-1" },
439
+ mockClient,
440
+ context
441
+ );
442
+
443
+ expect(mockClient.getAgent).toHaveBeenCalledWith("agent-1");
444
+ expect(result).toEqual(mockAgent);
445
+ });
446
+ });
447
+
448
+ describe("list_channels", () => {
449
+ it("should list channels", async () => {
450
+ // When not registered, result is NOT wrapped
451
+ const mockResult = { channels: [], total: 0 };
452
+ vi.mocked(mockClient.listChannels).mockResolvedValue(mockResult);
453
+
454
+ const result = await handleToolCall(
455
+ "list_channels",
456
+ {},
457
+ mockClient,
458
+ context
459
+ );
460
+
461
+ expect(mockClient.listChannels).toHaveBeenCalled();
462
+ expect(result).toEqual(mockResult);
463
+ });
464
+ });
465
+
466
+ describe("join_channel", () => {
467
+ it("should join channel", async () => {
468
+ context.setCurrentAgentId("test-agent");
469
+ const mockResult = { subscribed: true };
470
+ vi.mocked(mockClient.joinChannel).mockResolvedValue(mockResult);
471
+
472
+ const result = await handleToolCall(
473
+ "join_channel",
474
+ { channel: "team-channel" },
475
+ mockClient,
476
+ context
477
+ );
478
+
479
+ expect(mockClient.joinChannel).toHaveBeenCalledWith(
480
+ "team-channel",
481
+ "test-agent"
482
+ );
483
+ // Result is wrapped with messages
484
+ expect(result).toMatchObject({ result: mockResult });
485
+ });
486
+ });
487
+
488
+ describe("leave_channel", () => {
489
+ it("should leave channel", async () => {
490
+ context.setCurrentAgentId("test-agent");
491
+ const mockResult = { unsubscribed: true };
492
+ vi.mocked(mockClient.leaveChannel).mockResolvedValue(mockResult);
493
+
494
+ const result = await handleToolCall(
495
+ "leave_channel",
496
+ { channel: "team-channel" },
497
+ mockClient,
498
+ context
499
+ );
500
+
501
+ expect(mockClient.leaveChannel).toHaveBeenCalledWith(
502
+ "team-channel",
503
+ "test-agent"
504
+ );
505
+ // Result is wrapped with messages
506
+ expect(result).toMatchObject({ result: mockResult });
507
+ });
508
+ });
509
+
510
+ describe("unknown tool", () => {
511
+ it("should throw for unknown tool", async () => {
512
+ await expect(
513
+ handleToolCall("unknown_tool", {}, mockClient, context)
514
+ ).rejects.toThrow("Unknown tool: unknown_tool");
515
+ });
516
+ });
517
+ });