opencode-swarm-plugin 0.25.2 → 0.25.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,1429 +0,0 @@
1
- /**
2
- * Integration tests for agent-mail.ts
3
- *
4
- * These tests run against a real Agent Mail server (typically in Docker).
5
- * Set AGENT_MAIL_URL environment variable to override the default server location.
6
- *
7
- * Run with: pnpm test:integration
8
- * Or in Docker: docker compose up --build --abort-on-container-exit
9
- */
10
-
11
- import { describe, it, expect, beforeAll } from "vitest";
12
- import {
13
- mcpCall,
14
- mcpCallWithAutoInit,
15
- sessionStates,
16
- setState,
17
- clearState,
18
- requireState,
19
- MAX_INBOX_LIMIT,
20
- AgentMailNotInitializedError,
21
- isProjectNotFoundError,
22
- isAgentNotFoundError,
23
- type AgentMailState,
24
- } from "./agent-mail";
25
-
26
- // ============================================================================
27
- // Test Configuration
28
- // ============================================================================
29
-
30
- const AGENT_MAIL_URL = process.env.AGENT_MAIL_URL || "http://127.0.0.1:8765";
31
-
32
- /**
33
- * Generate a unique test context to avoid state collisions between tests
34
- */
35
- function createTestContext() {
36
- const id = `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
37
- return {
38
- sessionID: id,
39
- projectKey: `/test/project-${id}`,
40
- };
41
- }
42
-
43
- /**
44
- * Initialize a test agent and return its state
45
- */
46
- async function initTestAgent(
47
- ctx: { sessionID: string; projectKey: string },
48
- agentName?: string,
49
- ) {
50
- // Ensure project exists
51
- const project = await mcpCall<{
52
- id: number;
53
- slug: string;
54
- human_key: string;
55
- }>("ensure_project", { human_key: ctx.projectKey });
56
-
57
- // Register agent
58
- const agent = await mcpCall<{
59
- id: number;
60
- name: string;
61
- program: string;
62
- model: string;
63
- task_description: string;
64
- }>("register_agent", {
65
- project_key: ctx.projectKey,
66
- program: "opencode-test",
67
- model: "test-model",
68
- name: agentName,
69
- task_description: "Integration test agent",
70
- });
71
-
72
- // Store state
73
- const state: AgentMailState = {
74
- projectKey: ctx.projectKey,
75
- agentName: agent.name,
76
- reservations: [],
77
- startedAt: new Date().toISOString(),
78
- };
79
- setState(ctx.sessionID, state);
80
-
81
- return { project, agent, state };
82
- }
83
-
84
- // ============================================================================
85
- // Health Check Tests
86
- // ============================================================================
87
-
88
- describe("agent-mail integration", () => {
89
- beforeAll(async () => {
90
- // Verify server is reachable before running tests
91
- const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`);
92
- if (!response.ok) {
93
- throw new Error(
94
- `Agent Mail server not available at ${AGENT_MAIL_URL}. ` +
95
- `Start it with: docker compose up agent-mail`,
96
- );
97
- }
98
- });
99
-
100
- describe("agentmail_health", () => {
101
- it("returns healthy when server is running", async () => {
102
- const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`);
103
- expect(response.ok).toBe(true);
104
- const data = await response.json();
105
- // Real Agent Mail returns "alive" not "ok"
106
- expect(data.status).toBe("alive");
107
- });
108
-
109
- it("returns ready when database is accessible", async () => {
110
- const response = await fetch(`${AGENT_MAIL_URL}/health/readiness`);
111
- expect(response.ok).toBe(true);
112
- const data = await response.json();
113
- expect(data.status).toBe("ready");
114
- });
115
- });
116
-
117
- // ============================================================================
118
- // Initialization Tests
119
- // ============================================================================
120
-
121
- describe("agentmail_init", () => {
122
- it("creates project and registers agent", async () => {
123
- const ctx = createTestContext();
124
-
125
- const { project, agent, state } = await initTestAgent(ctx);
126
-
127
- expect(project.id).toBeGreaterThan(0);
128
- expect(project.human_key).toBe(ctx.projectKey);
129
- expect(agent.id).toBeGreaterThan(0);
130
- expect(agent.name).toBeTruthy();
131
- expect(state.projectKey).toBe(ctx.projectKey);
132
- expect(state.agentName).toBe(agent.name);
133
-
134
- // Cleanup
135
- clearState(ctx.sessionID);
136
- });
137
-
138
- it("generates unique agent name when not provided", async () => {
139
- const ctx = createTestContext();
140
-
141
- const { agent: agent1 } = await initTestAgent(ctx);
142
- clearState(ctx.sessionID);
143
-
144
- // Register another agent without name
145
- const ctx2 = { ...createTestContext(), projectKey: ctx.projectKey };
146
- const { agent: agent2 } = await initTestAgent(ctx2);
147
-
148
- // Both should have adjective+noun style names
149
- expect(agent1.name).toMatch(/^[A-Z][a-z]+[A-Z][a-z]+$/);
150
- expect(agent2.name).toMatch(/^[A-Z][a-z]+[A-Z][a-z]+$/);
151
-
152
- // Cleanup
153
- clearState(ctx.sessionID);
154
- clearState(ctx2.sessionID);
155
- });
156
-
157
- it("uses provided agent name when specified (valid adjective+noun)", async () => {
158
- const ctx = createTestContext();
159
- // Server has a specific word list - use known-valid combinations
160
- // Valid: BlueLake, GreenDog, RedStone, BlueBear
161
- const customName = "BlueLake";
162
-
163
- const { agent } = await initTestAgent(ctx, customName);
164
-
165
- expect(agent.name).toBe(customName);
166
-
167
- // Cleanup
168
- clearState(ctx.sessionID);
169
- });
170
-
171
- it("re-registering same name updates existing agent (dedup by name)", async () => {
172
- // Note: Real Agent Mail deduplicates by name within a project
173
- // Re-registering with same name updates the existing agent (same ID)
174
- const ctx = createTestContext();
175
- // Use a valid name from the server's word list
176
- const customName = "GreenDog";
177
-
178
- const { agent: agent1 } = await initTestAgent(ctx, customName);
179
- clearState(ctx.sessionID);
180
-
181
- // Re-register with same name - updates existing agent
182
- const ctx2 = { ...createTestContext(), projectKey: ctx.projectKey };
183
- const { agent: agent2 } = await initTestAgent(ctx2, customName);
184
-
185
- // Same name, same ID (updated, not duplicated)
186
- expect(agent1.name).toBe(agent2.name);
187
- expect(agent1.id).toBe(agent2.id);
188
-
189
- // Cleanup
190
- clearState(ctx2.sessionID);
191
- });
192
- });
193
-
194
- // ============================================================================
195
- // State Management Tests
196
- // ============================================================================
197
-
198
- describe("state management", () => {
199
- it("requireState throws when not initialized", () => {
200
- const sessionID = "nonexistent-session";
201
-
202
- expect(() => requireState(sessionID)).toThrow(
203
- AgentMailNotInitializedError,
204
- );
205
- });
206
-
207
- it("requireState returns state when initialized", async () => {
208
- const ctx = createTestContext();
209
- await initTestAgent(ctx);
210
-
211
- const state = requireState(ctx.sessionID);
212
- expect(state.projectKey).toBe(ctx.projectKey);
213
-
214
- // Cleanup
215
- clearState(ctx.sessionID);
216
- });
217
-
218
- it("clearState removes session state", async () => {
219
- const ctx = createTestContext();
220
- await initTestAgent(ctx);
221
-
222
- expect(sessionStates.has(ctx.sessionID)).toBe(true);
223
- clearState(ctx.sessionID);
224
- expect(sessionStates.has(ctx.sessionID)).toBe(false);
225
- });
226
- });
227
-
228
- // ============================================================================
229
- // Messaging Tests
230
- // ============================================================================
231
-
232
- describe("agentmail_send", () => {
233
- it("sends message to another agent", async () => {
234
- const ctx = createTestContext();
235
- const { state: senderState } = await initTestAgent(
236
- ctx,
237
- `Sender_${Date.now()}`,
238
- );
239
-
240
- // Create recipient agent
241
- const recipientCtx = {
242
- ...createTestContext(),
243
- projectKey: ctx.projectKey,
244
- };
245
- const { state: recipientState } = await initTestAgent(
246
- recipientCtx,
247
- `Recipient_${Date.now()}`,
248
- );
249
-
250
- // Send message
251
- // Real Agent Mail returns { deliveries: [{ payload: { id, subject, ... } }], count }
252
- const result = await mcpCall<{
253
- deliveries: Array<{
254
- payload: { id: number; subject: string; to: string[] };
255
- }>;
256
- count: number;
257
- }>("send_message", {
258
- project_key: senderState.projectKey,
259
- sender_name: senderState.agentName,
260
- to: [recipientState.agentName],
261
- subject: "Test message",
262
- body_md: "This is a test message body",
263
- thread_id: "bd-test-123",
264
- importance: "normal",
265
- ack_required: false,
266
- });
267
-
268
- expect(result.count).toBe(1);
269
- expect(result.deliveries[0].payload.id).toBeGreaterThan(0);
270
- expect(result.deliveries[0].payload.subject).toBe("Test message");
271
- expect(result.deliveries[0].payload.to).toContain(
272
- recipientState.agentName,
273
- );
274
-
275
- // Cleanup
276
- clearState(ctx.sessionID);
277
- clearState(recipientCtx.sessionID);
278
- });
279
-
280
- it("sends urgent message with ack_required", async () => {
281
- const ctx = createTestContext();
282
- const { state: senderState } = await initTestAgent(
283
- ctx,
284
- `UrgentSender_${Date.now()}`,
285
- );
286
-
287
- const recipientCtx = {
288
- ...createTestContext(),
289
- projectKey: ctx.projectKey,
290
- };
291
- const { state: recipientState } = await initTestAgent(
292
- recipientCtx,
293
- `UrgentRecipient_${Date.now()}`,
294
- );
295
-
296
- // Real Agent Mail returns { deliveries: [...], count }
297
- const result = await mcpCall<{
298
- deliveries: Array<{ payload: { id: number } }>;
299
- count: number;
300
- }>("send_message", {
301
- project_key: senderState.projectKey,
302
- sender_name: senderState.agentName,
303
- to: [recipientState.agentName],
304
- subject: "Urgent: Action required",
305
- body_md: "Please acknowledge this message",
306
- importance: "urgent",
307
- ack_required: true,
308
- });
309
-
310
- expect(result.count).toBe(1);
311
- expect(result.deliveries[0].payload.id).toBeGreaterThan(0);
312
-
313
- // Cleanup
314
- clearState(ctx.sessionID);
315
- clearState(recipientCtx.sessionID);
316
- });
317
- });
318
-
319
- // ============================================================================
320
- // Inbox Tests
321
- // ============================================================================
322
-
323
- describe("agentmail_inbox", () => {
324
- it("fetches messages without bodies by default (context-safe)", async () => {
325
- const ctx = createTestContext();
326
- const { state: senderState } = await initTestAgent(
327
- ctx,
328
- `InboxSender_${Date.now()}`,
329
- );
330
-
331
- const recipientCtx = {
332
- ...createTestContext(),
333
- projectKey: ctx.projectKey,
334
- };
335
- const { state: recipientState } = await initTestAgent(
336
- recipientCtx,
337
- `InboxRecipient_${Date.now()}`,
338
- );
339
-
340
- // Send a message
341
- await mcpCall("send_message", {
342
- project_key: senderState.projectKey,
343
- sender_name: senderState.agentName,
344
- to: [recipientState.agentName],
345
- subject: "Inbox test message",
346
- body_md: "This body should NOT be included by default",
347
- });
348
-
349
- // Fetch inbox WITHOUT bodies
350
- // Real Agent Mail returns { result: [...] } wrapper
351
- const response = await mcpCall<{
352
- result: Array<{
353
- id: number;
354
- subject: string;
355
- from: string;
356
- body_md?: string;
357
- }>;
358
- }>("fetch_inbox", {
359
- project_key: recipientState.projectKey,
360
- agent_name: recipientState.agentName,
361
- limit: 5,
362
- include_bodies: false, // MANDATORY context-safe default
363
- });
364
-
365
- const messages = response.result;
366
- expect(messages.length).toBeGreaterThan(0);
367
- const testMsg = messages.find((m) => m.subject === "Inbox test message");
368
- expect(testMsg).toBeDefined();
369
- expect(testMsg?.from).toBe(senderState.agentName);
370
- // Body should NOT be included when include_bodies: false
371
- expect(testMsg?.body_md).toBeUndefined();
372
-
373
- // Cleanup
374
- clearState(ctx.sessionID);
375
- clearState(recipientCtx.sessionID);
376
- });
377
-
378
- it("enforces MAX_INBOX_LIMIT constraint", async () => {
379
- const ctx = createTestContext();
380
- const { state: senderState } = await initTestAgent(
381
- ctx,
382
- `LimitSender_${Date.now()}`,
383
- );
384
-
385
- const recipientCtx = {
386
- ...createTestContext(),
387
- projectKey: ctx.projectKey,
388
- };
389
- const { state: recipientState } = await initTestAgent(
390
- recipientCtx,
391
- `LimitRecipient_${Date.now()}`,
392
- );
393
-
394
- // Send more messages than MAX_INBOX_LIMIT
395
- const messageCount = MAX_INBOX_LIMIT + 3;
396
- for (let i = 0; i < messageCount; i++) {
397
- await mcpCall("send_message", {
398
- project_key: senderState.projectKey,
399
- sender_name: senderState.agentName,
400
- to: [recipientState.agentName],
401
- subject: `Limit test message ${i}`,
402
- body_md: `Message body ${i}`,
403
- });
404
- }
405
-
406
- // Request more than MAX_INBOX_LIMIT
407
- const response = await mcpCall<{ result: Array<{ id: number }> }>(
408
- "fetch_inbox",
409
- {
410
- project_key: recipientState.projectKey,
411
- agent_name: recipientState.agentName,
412
- limit: messageCount, // Request more than allowed
413
- include_bodies: false,
414
- },
415
- );
416
-
417
- // Should still return the requested amount from server
418
- // The constraint enforcement happens in the tool wrapper, not mcpCall
419
- expect(response.result.length).toBeGreaterThanOrEqual(MAX_INBOX_LIMIT);
420
-
421
- // Cleanup
422
- clearState(ctx.sessionID);
423
- clearState(recipientCtx.sessionID);
424
- });
425
-
426
- it("filters urgent messages when urgent_only is true", async () => {
427
- const ctx = createTestContext();
428
- const { state: senderState } = await initTestAgent(
429
- ctx,
430
- `UrgentFilterSender_${Date.now()}`,
431
- );
432
-
433
- const recipientCtx = {
434
- ...createTestContext(),
435
- projectKey: ctx.projectKey,
436
- };
437
- const { state: recipientState } = await initTestAgent(
438
- recipientCtx,
439
- `UrgentFilterRecipient_${Date.now()}`,
440
- );
441
-
442
- // Send normal and urgent messages
443
- await mcpCall("send_message", {
444
- project_key: senderState.projectKey,
445
- sender_name: senderState.agentName,
446
- to: [recipientState.agentName],
447
- subject: "Normal message",
448
- body_md: "Not urgent",
449
- importance: "normal",
450
- });
451
-
452
- await mcpCall("send_message", {
453
- project_key: senderState.projectKey,
454
- sender_name: senderState.agentName,
455
- to: [recipientState.agentName],
456
- subject: "Urgent message",
457
- body_md: "Very urgent!",
458
- importance: "urgent",
459
- });
460
-
461
- // Fetch only urgent messages
462
- const response = await mcpCall<{
463
- result: Array<{ subject: string; importance: string }>;
464
- }>("fetch_inbox", {
465
- project_key: recipientState.projectKey,
466
- agent_name: recipientState.agentName,
467
- limit: 10,
468
- include_bodies: false,
469
- urgent_only: true,
470
- });
471
-
472
- const messages = response.result;
473
- // All returned messages should be urgent
474
- for (const msg of messages) {
475
- expect(msg.importance).toBe("urgent");
476
- }
477
- expect(messages.some((m) => m.subject === "Urgent message")).toBe(true);
478
-
479
- // Cleanup
480
- clearState(ctx.sessionID);
481
- clearState(recipientCtx.sessionID);
482
- });
483
-
484
- it("filters by since_ts timestamp", async () => {
485
- const ctx = createTestContext();
486
- const { state: senderState } = await initTestAgent(
487
- ctx,
488
- `TimeSender_${Date.now()}`,
489
- );
490
-
491
- const recipientCtx = {
492
- ...createTestContext(),
493
- projectKey: ctx.projectKey,
494
- };
495
- const { state: recipientState } = await initTestAgent(
496
- recipientCtx,
497
- `TimeRecipient_${Date.now()}`,
498
- );
499
-
500
- // Send first message
501
- await mcpCall("send_message", {
502
- project_key: senderState.projectKey,
503
- sender_name: senderState.agentName,
504
- to: [recipientState.agentName],
505
- subject: "Old message",
506
- body_md: "Sent before timestamp",
507
- });
508
-
509
- // Wait a moment and capture timestamp
510
- await new Promise((resolve) => setTimeout(resolve, 100));
511
- const sinceTs = new Date().toISOString();
512
- await new Promise((resolve) => setTimeout(resolve, 100));
513
-
514
- // Send second message after timestamp
515
- await mcpCall("send_message", {
516
- project_key: senderState.projectKey,
517
- sender_name: senderState.agentName,
518
- to: [recipientState.agentName],
519
- subject: "New message",
520
- body_md: "Sent after timestamp",
521
- });
522
-
523
- // Fetch only messages after timestamp
524
- const response = await mcpCall<{
525
- result: Array<{ subject: string }>;
526
- }>("fetch_inbox", {
527
- project_key: recipientState.projectKey,
528
- agent_name: recipientState.agentName,
529
- limit: 10,
530
- include_bodies: false,
531
- since_ts: sinceTs,
532
- });
533
-
534
- const messages = response.result;
535
- expect(messages.some((m) => m.subject === "New message")).toBe(true);
536
- expect(messages.some((m) => m.subject === "Old message")).toBe(false);
537
-
538
- // Cleanup
539
- clearState(ctx.sessionID);
540
- clearState(recipientCtx.sessionID);
541
- });
542
- });
543
-
544
- // ============================================================================
545
- // Read Message Tests
546
- // ============================================================================
547
-
548
- describe("agentmail_read_message", () => {
549
- it("marks message as read", async () => {
550
- const ctx = createTestContext();
551
- const { state: senderState } = await initTestAgent(
552
- ctx,
553
- `ReadSender_${Date.now()}`,
554
- );
555
-
556
- const recipientCtx = {
557
- ...createTestContext(),
558
- projectKey: ctx.projectKey,
559
- };
560
- const { state: recipientState } = await initTestAgent(
561
- recipientCtx,
562
- `ReadRecipient_${Date.now()}`,
563
- );
564
-
565
- // Send a message
566
- // Real Agent Mail returns { deliveries: [{ payload: { id, ... } }] }
567
- const sentMsg = await mcpCall<{
568
- deliveries: Array<{ payload: { id: number } }>;
569
- }>("send_message", {
570
- project_key: senderState.projectKey,
571
- sender_name: senderState.agentName,
572
- to: [recipientState.agentName],
573
- subject: "Read test message",
574
- body_md: "This message will be marked as read",
575
- });
576
-
577
- const messageId = sentMsg.deliveries[0].payload.id;
578
-
579
- // Mark as read
580
- // Real Agent Mail returns { message_id, read: bool, read_at: iso8601 | null }
581
- const result = await mcpCall<{
582
- message_id: number;
583
- read: boolean;
584
- read_at: string | null;
585
- }>("mark_message_read", {
586
- project_key: recipientState.projectKey,
587
- agent_name: recipientState.agentName,
588
- message_id: messageId,
589
- });
590
-
591
- expect(result.message_id).toBe(messageId);
592
- expect(result.read).toBe(true);
593
- expect(result.read_at).toBeTruthy();
594
-
595
- // Cleanup
596
- clearState(ctx.sessionID);
597
- clearState(recipientCtx.sessionID);
598
- });
599
- });
600
-
601
- // ============================================================================
602
- // Thread Summary Tests
603
- // ============================================================================
604
-
605
- describe("agentmail_summarize_thread", () => {
606
- // Skip: summarize_thread requires LLM which may not be available
607
- it.skip("summarizes messages in a thread", async () => {
608
- const ctx = createTestContext();
609
- const { state: senderState } = await initTestAgent(
610
- ctx,
611
- `ThreadSender_${Date.now()}`,
612
- );
613
-
614
- const recipientCtx = {
615
- ...createTestContext(),
616
- projectKey: ctx.projectKey,
617
- };
618
- const { state: recipientState } = await initTestAgent(
619
- recipientCtx,
620
- `ThreadRecipient_${Date.now()}`,
621
- );
622
-
623
- const threadId = `thread-${Date.now()}`;
624
-
625
- // Send multiple messages in the same thread
626
- await mcpCall("send_message", {
627
- project_key: senderState.projectKey,
628
- sender_name: senderState.agentName,
629
- to: [recipientState.agentName],
630
- subject: "Thread message 1",
631
- body_md: "First message in thread",
632
- thread_id: threadId,
633
- });
634
-
635
- await mcpCall("send_message", {
636
- project_key: senderState.projectKey,
637
- sender_name: senderState.agentName,
638
- to: [recipientState.agentName],
639
- subject: "Thread message 2",
640
- body_md: "Second message in thread",
641
- thread_id: threadId,
642
- });
643
-
644
- // Get thread summary
645
- const summary = await mcpCall<{
646
- thread_id: string;
647
- summary: {
648
- participants: string[];
649
- key_points: string[];
650
- action_items: string[];
651
- total_messages: number;
652
- };
653
- }>("summarize_thread", {
654
- project_key: senderState.projectKey,
655
- thread_id: threadId,
656
- include_examples: false,
657
- });
658
-
659
- expect(summary.thread_id).toBe(threadId);
660
- expect(summary.summary.participants).toContain(senderState.agentName);
661
- expect(summary.summary.total_messages).toBe(2);
662
- expect(summary.summary.key_points.length).toBeGreaterThan(0);
663
-
664
- // Cleanup
665
- clearState(ctx.sessionID);
666
- clearState(recipientCtx.sessionID);
667
- });
668
-
669
- // Skip: summarize_thread requires LLM which may not be available
670
- it.skip("includes example messages when requested", async () => {
671
- const ctx = createTestContext();
672
- const { state: senderState } = await initTestAgent(
673
- ctx,
674
- `ExampleSender_${Date.now()}`,
675
- );
676
-
677
- const recipientCtx = {
678
- ...createTestContext(),
679
- projectKey: ctx.projectKey,
680
- };
681
- const { state: recipientState } = await initTestAgent(
682
- recipientCtx,
683
- `ExampleRecipient_${Date.now()}`,
684
- );
685
-
686
- const threadId = `example-thread-${Date.now()}`;
687
-
688
- await mcpCall("send_message", {
689
- project_key: senderState.projectKey,
690
- sender_name: senderState.agentName,
691
- to: [recipientState.agentName],
692
- subject: "Example thread message",
693
- body_md: "This should be in examples",
694
- thread_id: threadId,
695
- });
696
-
697
- // Get summary with examples
698
- const summary = await mcpCall<{
699
- thread_id: string;
700
- examples?: Array<{
701
- id: number;
702
- subject: string;
703
- from: string;
704
- body_md?: string;
705
- }>;
706
- }>("summarize_thread", {
707
- project_key: senderState.projectKey,
708
- thread_id: threadId,
709
- include_examples: true,
710
- });
711
-
712
- expect(summary.examples).toBeDefined();
713
- expect(summary.examples!.length).toBeGreaterThan(0);
714
- expect(summary.examples![0].subject).toBe("Example thread message");
715
- expect(summary.examples![0].body_md).toBe("This should be in examples");
716
-
717
- // Cleanup
718
- clearState(ctx.sessionID);
719
- clearState(recipientCtx.sessionID);
720
- });
721
- });
722
-
723
- // ============================================================================
724
- // File Reservation Tests
725
- // ============================================================================
726
-
727
- describe("agentmail_reserve", () => {
728
- it("grants file reservations", async () => {
729
- const ctx = createTestContext();
730
- const { state } = await initTestAgent(ctx, `ReserveAgent_${Date.now()}`);
731
-
732
- const result = await mcpCall<{
733
- granted: Array<{
734
- id: number;
735
- path_pattern: string;
736
- exclusive: boolean;
737
- reason: string;
738
- expires_ts: string;
739
- }>;
740
- conflicts: Array<{ path: string; holders: string[] }>;
741
- }>("file_reservation_paths", {
742
- project_key: state.projectKey,
743
- agent_name: state.agentName,
744
- paths: ["src/auth/**", "src/config.ts"],
745
- ttl_seconds: 3600,
746
- exclusive: true,
747
- reason: "bd-test-123: Working on auth",
748
- });
749
-
750
- expect(result.granted.length).toBe(2);
751
- expect(result.conflicts.length).toBe(0);
752
- expect(result.granted[0].exclusive).toBe(true);
753
- expect(result.granted[0].reason).toContain("bd-test-123");
754
-
755
- // Cleanup
756
- clearState(ctx.sessionID);
757
- });
758
-
759
- it("detects conflicts with exclusive reservations", async () => {
760
- const ctx = createTestContext();
761
- const { state: agent1State } = await initTestAgent(
762
- ctx,
763
- `ConflictAgent1_${Date.now()}`,
764
- );
765
-
766
- const agent2Ctx = { ...createTestContext(), projectKey: ctx.projectKey };
767
- const { state: agent2State } = await initTestAgent(
768
- agent2Ctx,
769
- `ConflictAgent2_${Date.now()}`,
770
- );
771
-
772
- const conflictPath = `src/conflict-${Date.now()}.ts`;
773
-
774
- // Agent 1 reserves the file
775
- const result1 = await mcpCall<{
776
- granted: Array<{ id: number }>;
777
- conflicts: Array<{ path: string; holders: string[] }>;
778
- }>("file_reservation_paths", {
779
- project_key: agent1State.projectKey,
780
- agent_name: agent1State.agentName,
781
- paths: [conflictPath],
782
- ttl_seconds: 3600,
783
- exclusive: true,
784
- });
785
-
786
- expect(result1.granted.length).toBe(1);
787
- expect(result1.conflicts.length).toBe(0);
788
-
789
- // Agent 2 tries to reserve the same file
790
- // Real Agent Mail GRANTS the reservation but ALSO reports conflicts
791
- // This is the expected behavior - it's a warning, not a block
792
- const result2 = await mcpCall<{
793
- granted: Array<{ id: number }>;
794
- conflicts: Array<{
795
- path: string;
796
- holders: Array<{ agent: string; path_pattern: string }>;
797
- }>;
798
- }>("file_reservation_paths", {
799
- project_key: agent2State.projectKey,
800
- agent_name: agent2State.agentName,
801
- paths: [conflictPath],
802
- ttl_seconds: 3600,
803
- exclusive: true,
804
- });
805
-
806
- // Server grants the reservation but reports conflicts
807
- expect(result2.granted.length).toBe(1);
808
- expect(result2.conflicts.length).toBe(1);
809
- expect(result2.conflicts[0].path).toBe(conflictPath);
810
- // holders is an array of objects with agent field
811
- expect(
812
- result2.conflicts[0].holders.some(
813
- (h) => h.agent === agent1State.agentName,
814
- ),
815
- ).toBe(true);
816
-
817
- // Cleanup
818
- clearState(ctx.sessionID);
819
- clearState(agent2Ctx.sessionID);
820
- });
821
-
822
- it("stores reservation IDs in state", async () => {
823
- const ctx = createTestContext();
824
- const { state } = await initTestAgent(ctx, `StateAgent_${Date.now()}`);
825
-
826
- const result = await mcpCall<{
827
- granted: Array<{ id: number }>;
828
- }>("file_reservation_paths", {
829
- project_key: state.projectKey,
830
- agent_name: state.agentName,
831
- paths: ["src/state-test.ts"],
832
- ttl_seconds: 3600,
833
- exclusive: true,
834
- });
835
-
836
- // Manually track reservations like the tool does
837
- const reservationIds = result.granted.map((r) => r.id);
838
- state.reservations = [...state.reservations, ...reservationIds];
839
- setState(ctx.sessionID, state);
840
-
841
- // Verify state was updated
842
- const updatedState = requireState(ctx.sessionID);
843
- expect(updatedState.reservations.length).toBeGreaterThan(0);
844
- expect(updatedState.reservations).toContain(result.granted[0].id);
845
-
846
- // Cleanup
847
- clearState(ctx.sessionID);
848
- });
849
- });
850
-
851
- // ============================================================================
852
- // Release Reservation Tests
853
- // ============================================================================
854
-
855
- describe("agentmail_release", () => {
856
- it("releases all reservations for an agent", async () => {
857
- const ctx = createTestContext();
858
- const { state } = await initTestAgent(ctx, `ReleaseAgent_${Date.now()}`);
859
-
860
- // Create reservations
861
- await mcpCall("file_reservation_paths", {
862
- project_key: state.projectKey,
863
- agent_name: state.agentName,
864
- paths: ["src/release-test-1.ts", "src/release-test-2.ts"],
865
- ttl_seconds: 3600,
866
- exclusive: true,
867
- });
868
-
869
- // Release all
870
- const result = await mcpCall<{ released: number; released_at: string }>(
871
- "release_file_reservations",
872
- {
873
- project_key: state.projectKey,
874
- agent_name: state.agentName,
875
- },
876
- );
877
-
878
- expect(result.released).toBe(2);
879
- expect(result.released_at).toBeTruthy();
880
-
881
- // Cleanup
882
- clearState(ctx.sessionID);
883
- });
884
-
885
- it("releases specific paths only", async () => {
886
- const ctx = createTestContext();
887
- const { state } = await initTestAgent(
888
- ctx,
889
- `SpecificReleaseAgent_${Date.now()}`,
890
- );
891
-
892
- const path1 = `src/specific-release-1-${Date.now()}.ts`;
893
- const path2 = `src/specific-release-2-${Date.now()}.ts`;
894
-
895
- // Create reservations
896
- await mcpCall("file_reservation_paths", {
897
- project_key: state.projectKey,
898
- agent_name: state.agentName,
899
- paths: [path1, path2],
900
- ttl_seconds: 3600,
901
- exclusive: true,
902
- });
903
-
904
- // Release only one path
905
- const result = await mcpCall<{ released: number }>(
906
- "release_file_reservations",
907
- {
908
- project_key: state.projectKey,
909
- agent_name: state.agentName,
910
- paths: [path1],
911
- },
912
- );
913
-
914
- expect(result.released).toBe(1);
915
-
916
- // Verify second path can still cause conflicts
917
- const agent2Ctx = { ...createTestContext(), projectKey: ctx.projectKey };
918
- const { state: agent2State } = await initTestAgent(
919
- agent2Ctx,
920
- `SpecificReleaseAgent2_${Date.now()}`,
921
- );
922
-
923
- const conflictResult = await mcpCall<{
924
- conflicts: Array<{ path: string }>;
925
- }>("file_reservation_paths", {
926
- project_key: agent2State.projectKey,
927
- agent_name: agent2State.agentName,
928
- paths: [path2],
929
- exclusive: true,
930
- });
931
-
932
- expect(conflictResult.conflicts.length).toBe(1);
933
-
934
- // Cleanup
935
- clearState(ctx.sessionID);
936
- clearState(agent2Ctx.sessionID);
937
- });
938
-
939
- it("releases by reservation IDs", async () => {
940
- const ctx = createTestContext();
941
- const { state } = await initTestAgent(
942
- ctx,
943
- `IdReleaseAgent_${Date.now()}`,
944
- );
945
-
946
- // Create reservations
947
- const reserveResult = await mcpCall<{
948
- granted: Array<{ id: number }>;
949
- }>("file_reservation_paths", {
950
- project_key: state.projectKey,
951
- agent_name: state.agentName,
952
- paths: ["src/id-release-1.ts", "src/id-release-2.ts"],
953
- ttl_seconds: 3600,
954
- exclusive: true,
955
- });
956
-
957
- const firstId = reserveResult.granted[0].id;
958
-
959
- // Release by ID
960
- const result = await mcpCall<{ released: number }>(
961
- "release_file_reservations",
962
- {
963
- project_key: state.projectKey,
964
- agent_name: state.agentName,
965
- file_reservation_ids: [firstId],
966
- },
967
- );
968
-
969
- expect(result.released).toBe(1);
970
-
971
- // Cleanup
972
- clearState(ctx.sessionID);
973
- });
974
- });
975
-
976
- // ============================================================================
977
- // Acknowledge Message Tests
978
- // ============================================================================
979
-
980
- describe("agentmail_ack", () => {
981
- it("acknowledges a message requiring acknowledgement", async () => {
982
- const ctx = createTestContext();
983
- const { state: senderState } = await initTestAgent(
984
- ctx,
985
- `AckSender_${Date.now()}`,
986
- );
987
-
988
- const recipientCtx = {
989
- ...createTestContext(),
990
- projectKey: ctx.projectKey,
991
- };
992
- const { state: recipientState } = await initTestAgent(
993
- recipientCtx,
994
- `AckRecipient_${Date.now()}`,
995
- );
996
-
997
- // Send message requiring ack
998
- // Real Agent Mail returns { deliveries: [{ payload: { id, ... } }] }
999
- const sentMsg = await mcpCall<{
1000
- deliveries: Array<{ payload: { id: number } }>;
1001
- }>("send_message", {
1002
- project_key: senderState.projectKey,
1003
- sender_name: senderState.agentName,
1004
- to: [recipientState.agentName],
1005
- subject: "Please acknowledge",
1006
- body_md: "This requires acknowledgement",
1007
- ack_required: true,
1008
- });
1009
-
1010
- const messageId = sentMsg.deliveries[0].payload.id;
1011
-
1012
- // Acknowledge
1013
- // Real Agent Mail returns { acknowledged: bool, acknowledged_at: iso8601 | null }
1014
- const result = await mcpCall<{
1015
- message_id: number;
1016
- acknowledged: boolean;
1017
- acknowledged_at: string | null;
1018
- }>("acknowledge_message", {
1019
- project_key: recipientState.projectKey,
1020
- agent_name: recipientState.agentName,
1021
- message_id: messageId,
1022
- });
1023
-
1024
- expect(result.message_id).toBe(messageId);
1025
- expect(result.acknowledged).toBe(true);
1026
- expect(result.acknowledged_at).toBeTruthy();
1027
-
1028
- // Cleanup
1029
- clearState(ctx.sessionID);
1030
- clearState(recipientCtx.sessionID);
1031
- });
1032
- });
1033
-
1034
- // ============================================================================
1035
- // Search Tests
1036
- // ============================================================================
1037
-
1038
- describe("agentmail_search", () => {
1039
- it("searches messages by keyword using FTS5", async () => {
1040
- const ctx = createTestContext();
1041
- const { state: senderState } = await initTestAgent(
1042
- ctx,
1043
- `SearchSender_${Date.now()}`,
1044
- );
1045
-
1046
- const recipientCtx = {
1047
- ...createTestContext(),
1048
- projectKey: ctx.projectKey,
1049
- };
1050
- const { state: recipientState } = await initTestAgent(
1051
- recipientCtx,
1052
- `SearchRecipient_${Date.now()}`,
1053
- );
1054
-
1055
- const uniqueKeyword = `unicorn${Date.now()}`;
1056
-
1057
- // Send messages with searchable content
1058
- await mcpCall("send_message", {
1059
- project_key: senderState.projectKey,
1060
- sender_name: senderState.agentName,
1061
- to: [recipientState.agentName],
1062
- subject: `Message about ${uniqueKeyword}`,
1063
- body_md: "This message contains the keyword",
1064
- });
1065
-
1066
- await mcpCall("send_message", {
1067
- project_key: senderState.projectKey,
1068
- sender_name: senderState.agentName,
1069
- to: [recipientState.agentName],
1070
- subject: "Unrelated message",
1071
- body_md: "This message is about something else",
1072
- });
1073
-
1074
- // Search
1075
- // Real Agent Mail returns { result: [...] }
1076
- const response = await mcpCall<{
1077
- result: Array<{ id: number; subject: string }>;
1078
- }>("search_messages", {
1079
- project_key: senderState.projectKey,
1080
- query: uniqueKeyword,
1081
- limit: 10,
1082
- });
1083
-
1084
- const results = response.result;
1085
- expect(results.length).toBeGreaterThan(0);
1086
- expect(results.every((r) => r.subject.includes(uniqueKeyword))).toBe(
1087
- true,
1088
- );
1089
-
1090
- // Cleanup
1091
- clearState(ctx.sessionID);
1092
- clearState(recipientCtx.sessionID);
1093
- });
1094
-
1095
- it("respects search limit", async () => {
1096
- const ctx = createTestContext();
1097
- const { state: senderState } = await initTestAgent(
1098
- ctx,
1099
- `LimitSearchSender_${Date.now()}`,
1100
- );
1101
-
1102
- const recipientCtx = {
1103
- ...createTestContext(),
1104
- projectKey: ctx.projectKey,
1105
- };
1106
- const { state: recipientState } = await initTestAgent(
1107
- recipientCtx,
1108
- `LimitSearchRecipient_${Date.now()}`,
1109
- );
1110
-
1111
- const keyword = `searchlimit${Date.now()}`;
1112
-
1113
- // Send multiple matching messages
1114
- for (let i = 0; i < 5; i++) {
1115
- await mcpCall("send_message", {
1116
- project_key: senderState.projectKey,
1117
- sender_name: senderState.agentName,
1118
- to: [recipientState.agentName],
1119
- subject: `${keyword} message ${i}`,
1120
- body_md: `Content with ${keyword}`,
1121
- });
1122
- }
1123
-
1124
- // Search with limit
1125
- // Real Agent Mail returns { result: [...] }
1126
- const response = await mcpCall<{
1127
- result: Array<{ id: number }>;
1128
- }>("search_messages", {
1129
- project_key: senderState.projectKey,
1130
- query: keyword,
1131
- limit: 2,
1132
- });
1133
-
1134
- expect(response.result.length).toBe(2);
1135
-
1136
- // Cleanup
1137
- clearState(ctx.sessionID);
1138
- clearState(recipientCtx.sessionID);
1139
- });
1140
- });
1141
-
1142
- // ============================================================================
1143
- // Error Handling Tests
1144
- // ============================================================================
1145
-
1146
- describe("error handling", () => {
1147
- it("throws on unknown tool", async () => {
1148
- // Real Agent Mail returns isError: true which mcpCall converts to throw
1149
- await expect(mcpCall("nonexistent_tool", {})).rejects.toThrow(
1150
- /Unknown tool/,
1151
- );
1152
- });
1153
-
1154
- it("throws on missing required parameters", async () => {
1155
- // Real Agent Mail returns validation error with isError: true
1156
- await expect(mcpCall("ensure_project", {})).rejects.toThrow(
1157
- /Missing required argument|validation error/,
1158
- );
1159
- });
1160
-
1161
- it("throws on invalid project reference", async () => {
1162
- // Real Agent Mail auto-creates projects, so this actually succeeds
1163
- // Instead test with a truly invalid operation
1164
- await expect(
1165
- mcpCall("register_agent", {
1166
- // Missing required project_key
1167
- program: "test",
1168
- model: "test",
1169
- }),
1170
- ).rejects.toThrow(/Missing required argument|validation error/);
1171
- });
1172
- });
1173
-
1174
- // ============================================================================
1175
- // Multi-Agent Coordination Tests
1176
- // ============================================================================
1177
-
1178
- describe("multi-agent coordination", () => {
1179
- it("enables communication between multiple agents", async () => {
1180
- const ctx = createTestContext();
1181
-
1182
- // Create 3 agents in the same project
1183
- const agent1Ctx = ctx;
1184
- const { state: agent1 } = await initTestAgent(
1185
- agent1Ctx,
1186
- `Coordinator_${Date.now()}`,
1187
- );
1188
-
1189
- const agent2Ctx = { ...createTestContext(), projectKey: ctx.projectKey };
1190
- const { state: agent2 } = await initTestAgent(
1191
- agent2Ctx,
1192
- `Worker1_${Date.now()}`,
1193
- );
1194
-
1195
- const agent3Ctx = { ...createTestContext(), projectKey: ctx.projectKey };
1196
- const { state: agent3 } = await initTestAgent(
1197
- agent3Ctx,
1198
- `Worker2_${Date.now()}`,
1199
- );
1200
-
1201
- // Coordinator broadcasts to workers
1202
- await mcpCall("send_message", {
1203
- project_key: agent1.projectKey,
1204
- sender_name: agent1.agentName,
1205
- to: [agent2.agentName, agent3.agentName],
1206
- subject: "Task assignment",
1207
- body_md: "Please complete your subtasks",
1208
- thread_id: "bd-epic-123",
1209
- importance: "high",
1210
- });
1211
-
1212
- // Verify both workers received the message
1213
- const worker1Response = await mcpCall<{
1214
- result: Array<{ subject: string }>;
1215
- }>("fetch_inbox", {
1216
- project_key: agent2.projectKey,
1217
- agent_name: agent2.agentName,
1218
- limit: 5,
1219
- include_bodies: false,
1220
- });
1221
-
1222
- const worker2Response = await mcpCall<{
1223
- result: Array<{ subject: string }>;
1224
- }>("fetch_inbox", {
1225
- project_key: agent3.projectKey,
1226
- agent_name: agent3.agentName,
1227
- limit: 5,
1228
- include_bodies: false,
1229
- });
1230
-
1231
- expect(
1232
- worker1Response.result.some((m) => m.subject === "Task assignment"),
1233
- ).toBe(true);
1234
- expect(
1235
- worker2Response.result.some((m) => m.subject === "Task assignment"),
1236
- ).toBe(true);
1237
-
1238
- // Cleanup
1239
- clearState(agent1Ctx.sessionID);
1240
- clearState(agent2Ctx.sessionID);
1241
- clearState(agent3Ctx.sessionID);
1242
- });
1243
-
1244
- it("prevents file conflicts in swarm scenarios", async () => {
1245
- const ctx = createTestContext();
1246
-
1247
- // Coordinator assigns different files to workers
1248
- const coordCtx = ctx;
1249
- await initTestAgent(coordCtx, `SwarmCoord_${Date.now()}`);
1250
-
1251
- const worker1Ctx = { ...createTestContext(), projectKey: ctx.projectKey };
1252
- const { state: worker1 } = await initTestAgent(
1253
- worker1Ctx,
1254
- `SwarmWorker1_${Date.now()}`,
1255
- );
1256
-
1257
- const worker2Ctx = { ...createTestContext(), projectKey: ctx.projectKey };
1258
- const { state: worker2 } = await initTestAgent(
1259
- worker2Ctx,
1260
- `SwarmWorker2_${Date.now()}`,
1261
- );
1262
-
1263
- const path1 = `src/swarm/file1-${Date.now()}.ts`;
1264
- const path2 = `src/swarm/file2-${Date.now()}.ts`;
1265
-
1266
- // Worker 1 reserves file 1
1267
- const res1 = await mcpCall<{
1268
- granted: Array<{ id: number }>;
1269
- conflicts: unknown[];
1270
- }>("file_reservation_paths", {
1271
- project_key: worker1.projectKey,
1272
- agent_name: worker1.agentName,
1273
- paths: [path1],
1274
- exclusive: true,
1275
- reason: "bd-subtask-1",
1276
- });
1277
-
1278
- // Worker 2 reserves file 2
1279
- const res2 = await mcpCall<{
1280
- granted: Array<{ id: number }>;
1281
- conflicts: unknown[];
1282
- }>("file_reservation_paths", {
1283
- project_key: worker2.projectKey,
1284
- agent_name: worker2.agentName,
1285
- paths: [path2],
1286
- exclusive: true,
1287
- reason: "bd-subtask-2",
1288
- });
1289
-
1290
- // Both should succeed (no conflicts)
1291
- expect(res1.granted.length).toBe(1);
1292
- expect(res1.conflicts.length).toBe(0);
1293
- expect(res2.granted.length).toBe(1);
1294
- expect(res2.conflicts.length).toBe(0);
1295
-
1296
- // Worker 1 tries to reserve file 2 (should conflict)
1297
- // Real Agent Mail returns holders as array of objects with agent field
1298
- const conflict = await mcpCall<{
1299
- conflicts: Array<{
1300
- path: string;
1301
- holders: Array<{ agent: string; path_pattern: string }>;
1302
- }>;
1303
- }>("file_reservation_paths", {
1304
- project_key: worker1.projectKey,
1305
- agent_name: worker1.agentName,
1306
- paths: [path2],
1307
- exclusive: true,
1308
- });
1309
-
1310
- expect(conflict.conflicts.length).toBe(1);
1311
- // holders is an array of objects with agent field
1312
- expect(
1313
- conflict.conflicts[0].holders.some(
1314
- (h) => h.agent === worker2.agentName,
1315
- ),
1316
- ).toBe(true);
1317
-
1318
- // Cleanup
1319
- clearState(coordCtx.sessionID);
1320
- clearState(worker1Ctx.sessionID);
1321
- clearState(worker2Ctx.sessionID);
1322
- });
1323
- });
1324
-
1325
- // ============================================================================
1326
- // Self-Healing Tests (mcpCallWithAutoInit)
1327
- // ============================================================================
1328
-
1329
- describe("self-healing (mcpCallWithAutoInit)", () => {
1330
- it("detects project not found errors correctly", () => {
1331
- const projectError = new Error("Project 'migrate-egghead' not found.");
1332
- const agentError = new Error("Agent 'BlueLake' not found in project");
1333
- const otherError = new Error("Network timeout");
1334
-
1335
- expect(isProjectNotFoundError(projectError)).toBe(true);
1336
- expect(isProjectNotFoundError(agentError)).toBe(false);
1337
- expect(isProjectNotFoundError(otherError)).toBe(false);
1338
-
1339
- expect(isAgentNotFoundError(agentError)).toBe(true);
1340
- expect(isAgentNotFoundError(projectError)).toBe(false);
1341
- expect(isAgentNotFoundError(otherError)).toBe(false);
1342
- });
1343
-
1344
- it("auto-registers project on 'not found' error", async () => {
1345
- const ctx = createTestContext();
1346
-
1347
- // First, ensure project exists and register an agent
1348
- const { state } = await initTestAgent(ctx, `AutoInit_${Date.now()}`);
1349
-
1350
- // Now use mcpCallWithAutoInit - it should work normally
1351
- // (no error to recover from, but verifies the wrapper works)
1352
- await mcpCallWithAutoInit("send_message", {
1353
- project_key: state.projectKey,
1354
- agent_name: state.agentName,
1355
- sender_name: state.agentName,
1356
- to: [],
1357
- subject: "Test auto-init wrapper",
1358
- body_md: "This should work normally",
1359
- thread_id: "test-thread",
1360
- importance: "normal",
1361
- });
1362
-
1363
- // Verify message was sent by checking inbox
1364
- const inbox = await mcpCall<Array<{ subject: string }>>("fetch_inbox", {
1365
- project_key: state.projectKey,
1366
- agent_name: state.agentName,
1367
- limit: 5,
1368
- include_bodies: false,
1369
- });
1370
-
1371
- // The message should be in the inbox (sent to empty 'to' = broadcast)
1372
- // Note: depending on Agent Mail behavior, broadcast might not show in sender's inbox
1373
- // This test mainly verifies the wrapper doesn't break normal operation
1374
-
1375
- // Cleanup
1376
- clearState(ctx.sessionID);
1377
- });
1378
-
1379
- it("recovers from simulated project not found by re-registering", async () => {
1380
- const ctx = createTestContext();
1381
-
1382
- // Create a fresh project key that doesn't exist yet
1383
- const freshProjectKey = `/test/fresh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1384
- const agentName = `Recovery_${Date.now()}`;
1385
-
1386
- // First ensure the project exists (simulating initial setup)
1387
- await mcpCall("ensure_project", { human_key: freshProjectKey });
1388
- await mcpCall("register_agent", {
1389
- project_key: freshProjectKey,
1390
- program: "opencode-test",
1391
- model: "test-model",
1392
- name: agentName,
1393
- task_description: "Recovery test agent",
1394
- });
1395
-
1396
- // Now use mcpCallWithAutoInit for an operation
1397
- // This should work, and if the project somehow got lost, it would re-register
1398
- await mcpCallWithAutoInit("send_message", {
1399
- project_key: freshProjectKey,
1400
- agent_name: agentName,
1401
- sender_name: agentName,
1402
- to: [],
1403
- subject: "Recovery test",
1404
- body_md: "Testing self-healing",
1405
- thread_id: "recovery-test",
1406
- importance: "normal",
1407
- });
1408
-
1409
- // If we got here without error, the wrapper is working
1410
- // (In a real scenario where the server restarted, it would have re-registered)
1411
- });
1412
-
1413
- it("passes through non-recoverable errors", async () => {
1414
- const ctx = createTestContext();
1415
- const { state } = await initTestAgent(ctx, `ErrorPass_${Date.now()}`);
1416
-
1417
- // Try to call a non-existent tool - should throw, not retry forever
1418
- await expect(
1419
- mcpCallWithAutoInit("nonexistent_tool_xyz", {
1420
- project_key: state.projectKey,
1421
- agent_name: state.agentName,
1422
- }),
1423
- ).rejects.toThrow(/Unknown tool/);
1424
-
1425
- // Cleanup
1426
- clearState(ctx.sessionID);
1427
- });
1428
- });
1429
- });