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.
- package/README.md +170 -151
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -1
- package/dist/client.js.map +1 -1
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -1
- package/dist/client.test.js +16 -1
- package/dist/client.test.js.map +1 -1
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.d.ts.map +1 -1
- package/dist/e2e.test.js +3 -1
- package/dist/e2e.test.js.map +1 -1
- package/dist/heartbeat.d.ts +11 -1
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +41 -3
- package/dist/heartbeat.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +13 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/tools.test.js +6 -1
- package/dist/tools/tools.test.js.map +1 -1
- package/package.json +53 -53
- package/src/client.test.ts +223 -208
- package/src/client.ts +293 -286
- package/src/e2e.test.ts +300 -298
- package/src/heartbeat.ts +45 -3
- package/src/index.ts +123 -123
- package/src/tools/index.ts +672 -666
- package/src/tools/tools.test.ts +522 -517
- package/vitest.config.ts +8 -0
package/src/tools/tools.test.ts
CHANGED
|
@@ -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 = {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
"
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
expect(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
expect(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
});
|