agenthub-multiagent-mcp 1.1.2 → 1.1.4

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,522 @@
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 = {
182
+ acknowledged: true,
183
+ timestamp: "2024-01-01T00:00:00Z",
184
+ pending_tasks_count: 0,
185
+ unread_messages_count: 0,
186
+ };
187
+ vi.mocked(mockClient.heartbeat).mockResolvedValue(mockResult);
188
+
189
+ const result = await handleToolCall(
190
+ "agent_set_status",
191
+ { status: "busy" },
192
+ mockClient,
193
+ context
194
+ );
195
+
196
+ expect(mockClient.heartbeat).toHaveBeenCalledWith("test-agent", "busy");
197
+ // Result is wrapped with messages
198
+ expect(result).toMatchObject({ result: mockResult });
199
+ });
200
+ });
201
+
202
+ describe("agent_disconnect", () => {
203
+ it("should throw if not registered", async () => {
204
+ await expect(
205
+ handleToolCall("agent_disconnect", {}, mockClient, context)
206
+ ).rejects.toThrow("Not registered");
207
+ });
208
+
209
+ it("should disconnect and stop heartbeat when registered", async () => {
210
+ context.setCurrentAgentId("test-agent");
211
+ const mockResult = { unregistered: true };
212
+ vi.mocked(mockClient.disconnect).mockResolvedValue(mockResult);
213
+
214
+ const result = await handleToolCall(
215
+ "agent_disconnect",
216
+ {},
217
+ mockClient,
218
+ context
219
+ );
220
+
221
+ expect(context.stopHeartbeat).toHaveBeenCalled();
222
+ expect(mockClient.disconnect).toHaveBeenCalledWith("test-agent");
223
+ expect(result).toEqual(mockResult);
224
+ });
225
+ });
226
+
227
+ describe("send_message", () => {
228
+ it("should throw if not registered", async () => {
229
+ await expect(
230
+ handleToolCall(
231
+ "send_message",
232
+ { to: "other-agent", type: "task", subject: "Test", body: "Content" },
233
+ mockClient,
234
+ context
235
+ )
236
+ ).rejects.toThrow("Not registered");
237
+ });
238
+
239
+ it("should send message when registered", async () => {
240
+ context.setCurrentAgentId("test-agent");
241
+ const mockResult = {
242
+ message_id: "msg-1",
243
+ status: "pending",
244
+ created_at: "2024-01-01T00:00:00Z",
245
+ };
246
+ vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
247
+
248
+ const result = await handleToolCall(
249
+ "send_message",
250
+ {
251
+ to: "other-agent",
252
+ type: "task",
253
+ subject: "Test Task",
254
+ body: "Please do this",
255
+ priority: "high",
256
+ },
257
+ mockClient,
258
+ context
259
+ );
260
+
261
+ expect(mockClient.sendMessage).toHaveBeenCalledWith({
262
+ from_agent: "test-agent",
263
+ to_agent: "other-agent",
264
+ type: "task",
265
+ subject: "Test Task",
266
+ body: "Please do this",
267
+ priority: "high",
268
+ });
269
+ // Result is wrapped with messages
270
+ expect(result).toMatchObject({ result: mockResult });
271
+ });
272
+ });
273
+
274
+ describe("send_to_channel", () => {
275
+ it("should send message to channel", async () => {
276
+ context.setCurrentAgentId("test-agent");
277
+ const mockResult = {
278
+ message_id: "msg-1",
279
+ status: "pending",
280
+ created_at: "2024-01-01T00:00:00Z",
281
+ };
282
+ vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
283
+
284
+ const result = await handleToolCall(
285
+ "send_to_channel",
286
+ {
287
+ channel: "team-channel",
288
+ type: "status",
289
+ subject: "Update",
290
+ body: "Everything is good",
291
+ },
292
+ mockClient,
293
+ context
294
+ );
295
+
296
+ expect(mockClient.sendMessage).toHaveBeenCalledWith({
297
+ from_agent: "test-agent",
298
+ to_channel: "team-channel",
299
+ type: "status",
300
+ subject: "Update",
301
+ body: "Everything is good",
302
+ });
303
+ // Result is wrapped with messages
304
+ expect(result).toMatchObject({ result: mockResult });
305
+ });
306
+ });
307
+
308
+ describe("broadcast", () => {
309
+ it("should send broadcast message", async () => {
310
+ context.setCurrentAgentId("test-agent");
311
+ const mockResult = {
312
+ message_id: "msg-1",
313
+ status: "pending",
314
+ created_at: "2024-01-01T00:00:00Z",
315
+ };
316
+ vi.mocked(mockClient.sendMessage).mockResolvedValue(mockResult);
317
+
318
+ const result = await handleToolCall(
319
+ "broadcast",
320
+ {
321
+ type: "status",
322
+ subject: "System Alert",
323
+ body: "All systems operational",
324
+ },
325
+ mockClient,
326
+ context
327
+ );
328
+
329
+ expect(mockClient.sendMessage).toHaveBeenCalledWith({
330
+ from_agent: "test-agent",
331
+ broadcast: true,
332
+ type: "status",
333
+ subject: "System Alert",
334
+ body: "All systems operational",
335
+ });
336
+ // Result is wrapped with messages
337
+ expect(result).toMatchObject({ result: mockResult });
338
+ });
339
+ });
340
+
341
+ describe("check_inbox", () => {
342
+ it("should get inbox messages", async () => {
343
+ context.setCurrentAgentId("test-agent");
344
+ const mockResult = { messages: [], total: 0 };
345
+ vi.mocked(mockClient.getInbox).mockResolvedValue(mockResult);
346
+
347
+ const result = await handleToolCall(
348
+ "check_inbox",
349
+ { unread_only: true },
350
+ mockClient,
351
+ context
352
+ );
353
+
354
+ expect(mockClient.getInbox).toHaveBeenCalledWith("test-agent", true);
355
+ expect(result).toEqual(mockResult);
356
+ });
357
+ });
358
+
359
+ describe("mark_read", () => {
360
+ it("should mark message as read", async () => {
361
+ // When not registered, result is NOT wrapped
362
+ const mockResult = { updated: true };
363
+ vi.mocked(mockClient.markRead).mockResolvedValue(mockResult);
364
+
365
+ const result = await handleToolCall(
366
+ "mark_read",
367
+ { message_id: "msg-1" },
368
+ mockClient,
369
+ context
370
+ );
371
+
372
+ expect(mockClient.markRead).toHaveBeenCalledWith("msg-1");
373
+ expect(result).toEqual(mockResult);
374
+ });
375
+ });
376
+
377
+ describe("list_agents", () => {
378
+ it("should list agents without filter", async () => {
379
+ // When not registered, result is NOT wrapped
380
+ const mockResult = { agents: [], total: 0 };
381
+ vi.mocked(mockClient.listAgents).mockResolvedValue(mockResult);
382
+
383
+ const result = await handleToolCall("list_agents", {}, mockClient, context);
384
+
385
+ expect(mockClient.listAgents).toHaveBeenCalledWith(undefined);
386
+ expect(result).toEqual(mockResult);
387
+ });
388
+
389
+ it("should list agents with status filter", async () => {
390
+ // When not registered, result is NOT wrapped
391
+ const mockResult = {
392
+ agents: [
393
+ {
394
+ id: "agent-1",
395
+ name: "Agent 1",
396
+ owner: "test-user",
397
+ model: "claude-opus-4.5",
398
+ model_provider: "anthropic",
399
+ status: "online" as const,
400
+ working_dir: "",
401
+ current_task: "",
402
+ channels: [],
403
+ registered_at: "",
404
+ last_heartbeat: "",
405
+ },
406
+ ],
407
+ total: 1,
408
+ };
409
+ vi.mocked(mockClient.listAgents).mockResolvedValue(mockResult);
410
+
411
+ const result = await handleToolCall(
412
+ "list_agents",
413
+ { status: "online" },
414
+ mockClient,
415
+ context
416
+ );
417
+
418
+ expect(mockClient.listAgents).toHaveBeenCalledWith("online");
419
+ expect(result).toEqual(mockResult);
420
+ });
421
+ });
422
+
423
+ describe("get_agent", () => {
424
+ it("should get agent details", async () => {
425
+ // When not registered, result is NOT wrapped
426
+ const mockAgent = {
427
+ id: "agent-1",
428
+ name: "Agent 1",
429
+ owner: "test-user",
430
+ model: "claude-opus-4.5",
431
+ model_provider: "anthropic",
432
+ status: "online" as const,
433
+ working_dir: "/path",
434
+ current_task: "Task",
435
+ channels: [],
436
+ registered_at: "",
437
+ last_heartbeat: "",
438
+ };
439
+ vi.mocked(mockClient.getAgent).mockResolvedValue(mockAgent);
440
+
441
+ const result = await handleToolCall(
442
+ "get_agent",
443
+ { id: "agent-1" },
444
+ mockClient,
445
+ context
446
+ );
447
+
448
+ expect(mockClient.getAgent).toHaveBeenCalledWith("agent-1");
449
+ expect(result).toEqual(mockAgent);
450
+ });
451
+ });
452
+
453
+ describe("list_channels", () => {
454
+ it("should list channels", async () => {
455
+ // When not registered, result is NOT wrapped
456
+ const mockResult = { channels: [], total: 0 };
457
+ vi.mocked(mockClient.listChannels).mockResolvedValue(mockResult);
458
+
459
+ const result = await handleToolCall(
460
+ "list_channels",
461
+ {},
462
+ mockClient,
463
+ context
464
+ );
465
+
466
+ expect(mockClient.listChannels).toHaveBeenCalled();
467
+ expect(result).toEqual(mockResult);
468
+ });
469
+ });
470
+
471
+ describe("join_channel", () => {
472
+ it("should join channel", async () => {
473
+ context.setCurrentAgentId("test-agent");
474
+ const mockResult = { subscribed: true };
475
+ vi.mocked(mockClient.joinChannel).mockResolvedValue(mockResult);
476
+
477
+ const result = await handleToolCall(
478
+ "join_channel",
479
+ { channel: "team-channel" },
480
+ mockClient,
481
+ context
482
+ );
483
+
484
+ expect(mockClient.joinChannel).toHaveBeenCalledWith(
485
+ "team-channel",
486
+ "test-agent"
487
+ );
488
+ // Result is wrapped with messages
489
+ expect(result).toMatchObject({ result: mockResult });
490
+ });
491
+ });
492
+
493
+ describe("leave_channel", () => {
494
+ it("should leave channel", async () => {
495
+ context.setCurrentAgentId("test-agent");
496
+ const mockResult = { unsubscribed: true };
497
+ vi.mocked(mockClient.leaveChannel).mockResolvedValue(mockResult);
498
+
499
+ const result = await handleToolCall(
500
+ "leave_channel",
501
+ { channel: "team-channel" },
502
+ mockClient,
503
+ context
504
+ );
505
+
506
+ expect(mockClient.leaveChannel).toHaveBeenCalledWith(
507
+ "team-channel",
508
+ "test-agent"
509
+ );
510
+ // Result is wrapped with messages
511
+ expect(result).toMatchObject({ result: mockResult });
512
+ });
513
+ });
514
+
515
+ describe("unknown tool", () => {
516
+ it("should throw for unknown tool", async () => {
517
+ await expect(
518
+ handleToolCall("unknown_tool", {}, mockClient, context)
519
+ ).rejects.toThrow("Unknown tool: unknown_tool");
520
+ });
521
+ });
522
+ });