opencode-swarm-plugin 0.12.30 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.beads/issues.jsonl +204 -10
  2. package/.opencode/skills/tdd/SKILL.md +182 -0
  3. package/README.md +165 -17
  4. package/bin/swarm.ts +120 -31
  5. package/bun.lock +23 -0
  6. package/dist/index.js +4020 -438
  7. package/dist/pglite.data +0 -0
  8. package/dist/pglite.wasm +0 -0
  9. package/dist/plugin.js +4008 -514
  10. package/examples/commands/swarm.md +114 -19
  11. package/examples/skills/beads-workflow/SKILL.md +75 -28
  12. package/examples/skills/swarm-coordination/SKILL.md +92 -1
  13. package/global-skills/testing-patterns/SKILL.md +430 -0
  14. package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +586 -0
  15. package/package.json +11 -5
  16. package/src/index.ts +44 -5
  17. package/src/streams/agent-mail.test.ts +777 -0
  18. package/src/streams/agent-mail.ts +535 -0
  19. package/src/streams/debug.test.ts +500 -0
  20. package/src/streams/debug.ts +629 -0
  21. package/src/streams/effect/ask.integration.test.ts +314 -0
  22. package/src/streams/effect/ask.ts +202 -0
  23. package/src/streams/effect/cursor.integration.test.ts +418 -0
  24. package/src/streams/effect/cursor.ts +288 -0
  25. package/src/streams/effect/deferred.test.ts +357 -0
  26. package/src/streams/effect/deferred.ts +445 -0
  27. package/src/streams/effect/index.ts +17 -0
  28. package/src/streams/effect/layers.ts +73 -0
  29. package/src/streams/effect/lock.test.ts +385 -0
  30. package/src/streams/effect/lock.ts +399 -0
  31. package/src/streams/effect/mailbox.test.ts +260 -0
  32. package/src/streams/effect/mailbox.ts +318 -0
  33. package/src/streams/events.test.ts +628 -0
  34. package/src/streams/events.ts +214 -0
  35. package/src/streams/index.test.ts +229 -0
  36. package/src/streams/index.ts +492 -0
  37. package/src/streams/migrations.test.ts +355 -0
  38. package/src/streams/migrations.ts +269 -0
  39. package/src/streams/projections.test.ts +611 -0
  40. package/src/streams/projections.ts +302 -0
  41. package/src/streams/store.integration.test.ts +548 -0
  42. package/src/streams/store.ts +546 -0
  43. package/src/streams/swarm-mail.ts +552 -0
  44. package/src/swarm-mail.integration.test.ts +970 -0
  45. package/src/swarm-mail.ts +739 -0
  46. package/src/swarm.ts +84 -59
  47. package/src/tool-availability.ts +35 -2
  48. package/global-skills/mcp-tool-authoring/SKILL.md +0 -695
@@ -0,0 +1,628 @@
1
+ /**
2
+ * Unit tests for Event Types and Helpers
3
+ *
4
+ * Tests:
5
+ * - Schema validation for all event types
6
+ * - createEvent helper
7
+ * - isEventType type guard
8
+ * - Edge cases and error handling
9
+ */
10
+ import { describe, it, expect } from "vitest";
11
+ import {
12
+ AgentEventSchema,
13
+ AgentRegisteredEventSchema,
14
+ AgentActiveEventSchema,
15
+ MessageSentEventSchema,
16
+ MessageReadEventSchema,
17
+ MessageAckedEventSchema,
18
+ FileReservedEventSchema,
19
+ FileReleasedEventSchema,
20
+ TaskStartedEventSchema,
21
+ TaskProgressEventSchema,
22
+ TaskCompletedEventSchema,
23
+ TaskBlockedEventSchema,
24
+ createEvent,
25
+ isEventType,
26
+ type AgentEvent,
27
+ } from "./events";
28
+
29
+ // ============================================================================
30
+ // Schema Validation Tests
31
+ // ============================================================================
32
+
33
+ describe("AgentRegisteredEventSchema", () => {
34
+ it("validates a complete agent_registered event", () => {
35
+ const event = {
36
+ type: "agent_registered",
37
+ project_key: "/test/project",
38
+ timestamp: Date.now(),
39
+ agent_name: "BlueLake",
40
+ program: "opencode",
41
+ model: "claude-sonnet-4",
42
+ task_description: "Working on auth",
43
+ };
44
+ expect(() => AgentRegisteredEventSchema.parse(event)).not.toThrow();
45
+ });
46
+
47
+ it("applies defaults for program and model", () => {
48
+ const event = {
49
+ type: "agent_registered",
50
+ project_key: "/test/project",
51
+ timestamp: Date.now(),
52
+ agent_name: "BlueLake",
53
+ };
54
+ const parsed = AgentRegisteredEventSchema.parse(event);
55
+ expect(parsed.program).toBe("opencode");
56
+ expect(parsed.model).toBe("unknown");
57
+ });
58
+
59
+ it("rejects missing agent_name", () => {
60
+ const event = {
61
+ type: "agent_registered",
62
+ project_key: "/test/project",
63
+ timestamp: Date.now(),
64
+ };
65
+ expect(() => AgentRegisteredEventSchema.parse(event)).toThrow();
66
+ });
67
+ });
68
+
69
+ describe("AgentActiveEventSchema", () => {
70
+ it("validates agent_active event", () => {
71
+ const event = {
72
+ type: "agent_active",
73
+ project_key: "/test/project",
74
+ timestamp: Date.now(),
75
+ agent_name: "BlueLake",
76
+ };
77
+ expect(() => AgentActiveEventSchema.parse(event)).not.toThrow();
78
+ });
79
+ });
80
+
81
+ describe("MessageSentEventSchema", () => {
82
+ it("validates a complete message_sent event", () => {
83
+ const event = {
84
+ type: "message_sent",
85
+ project_key: "/test/project",
86
+ timestamp: Date.now(),
87
+ from_agent: "BlueLake",
88
+ to_agents: ["RedStone", "GreenCastle"],
89
+ subject: "Task update",
90
+ body: "Completed the auth module",
91
+ thread_id: "bd-123",
92
+ importance: "high",
93
+ ack_required: true,
94
+ };
95
+ expect(() => MessageSentEventSchema.parse(event)).not.toThrow();
96
+ });
97
+
98
+ it("applies defaults for importance and ack_required", () => {
99
+ const event = {
100
+ type: "message_sent",
101
+ project_key: "/test/project",
102
+ timestamp: Date.now(),
103
+ from_agent: "BlueLake",
104
+ to_agents: ["RedStone"],
105
+ subject: "Hello",
106
+ body: "World",
107
+ };
108
+ const parsed = MessageSentEventSchema.parse(event);
109
+ expect(parsed.importance).toBe("normal");
110
+ expect(parsed.ack_required).toBe(false);
111
+ });
112
+
113
+ it("validates importance enum values", () => {
114
+ const validImportance = ["low", "normal", "high", "urgent"];
115
+ for (const importance of validImportance) {
116
+ const event = {
117
+ type: "message_sent",
118
+ project_key: "/test/project",
119
+ timestamp: Date.now(),
120
+ from_agent: "BlueLake",
121
+ to_agents: ["RedStone"],
122
+ subject: "Test",
123
+ body: "Test",
124
+ importance,
125
+ };
126
+ expect(() => MessageSentEventSchema.parse(event)).not.toThrow();
127
+ }
128
+ });
129
+
130
+ it("rejects invalid importance value", () => {
131
+ const event = {
132
+ type: "message_sent",
133
+ project_key: "/test/project",
134
+ timestamp: Date.now(),
135
+ from_agent: "BlueLake",
136
+ to_agents: ["RedStone"],
137
+ subject: "Test",
138
+ body: "Test",
139
+ importance: "critical", // Invalid
140
+ };
141
+ expect(() => MessageSentEventSchema.parse(event)).toThrow();
142
+ });
143
+
144
+ it("rejects empty to_agents array", () => {
145
+ const event = {
146
+ type: "message_sent",
147
+ project_key: "/test/project",
148
+ timestamp: Date.now(),
149
+ from_agent: "BlueLake",
150
+ to_agents: [],
151
+ subject: "Test",
152
+ body: "Test",
153
+ };
154
+ // Empty array is technically valid per schema - it's a broadcast
155
+ expect(() => MessageSentEventSchema.parse(event)).not.toThrow();
156
+ });
157
+ });
158
+
159
+ describe("MessageReadEventSchema", () => {
160
+ it("validates message_read event", () => {
161
+ const event = {
162
+ type: "message_read",
163
+ project_key: "/test/project",
164
+ timestamp: Date.now(),
165
+ message_id: 42,
166
+ agent_name: "RedStone",
167
+ };
168
+ expect(() => MessageReadEventSchema.parse(event)).not.toThrow();
169
+ });
170
+ });
171
+
172
+ describe("MessageAckedEventSchema", () => {
173
+ it("validates message_acked event", () => {
174
+ const event = {
175
+ type: "message_acked",
176
+ project_key: "/test/project",
177
+ timestamp: Date.now(),
178
+ message_id: 42,
179
+ agent_name: "RedStone",
180
+ };
181
+ expect(() => MessageAckedEventSchema.parse(event)).not.toThrow();
182
+ });
183
+ });
184
+
185
+ describe("FileReservedEventSchema", () => {
186
+ it("validates a complete file_reserved event", () => {
187
+ const event = {
188
+ type: "file_reserved",
189
+ project_key: "/test/project",
190
+ timestamp: Date.now(),
191
+ agent_name: "BlueLake",
192
+ paths: ["src/auth/**", "src/config.ts"],
193
+ reason: "bd-123: Working on auth",
194
+ exclusive: true,
195
+ ttl_seconds: 3600,
196
+ expires_at: Date.now() + 3600000,
197
+ };
198
+ expect(() => FileReservedEventSchema.parse(event)).not.toThrow();
199
+ });
200
+
201
+ it("applies defaults for exclusive and ttl_seconds", () => {
202
+ const event = {
203
+ type: "file_reserved",
204
+ project_key: "/test/project",
205
+ timestamp: Date.now(),
206
+ agent_name: "BlueLake",
207
+ paths: ["src/auth/**"],
208
+ expires_at: Date.now() + 3600000,
209
+ };
210
+ const parsed = FileReservedEventSchema.parse(event);
211
+ expect(parsed.exclusive).toBe(true);
212
+ expect(parsed.ttl_seconds).toBe(3600);
213
+ });
214
+
215
+ it("requires expires_at", () => {
216
+ const event = {
217
+ type: "file_reserved",
218
+ project_key: "/test/project",
219
+ timestamp: Date.now(),
220
+ agent_name: "BlueLake",
221
+ paths: ["src/auth/**"],
222
+ };
223
+ expect(() => FileReservedEventSchema.parse(event)).toThrow();
224
+ });
225
+ });
226
+
227
+ describe("FileReleasedEventSchema", () => {
228
+ it("validates file_released with paths", () => {
229
+ const event = {
230
+ type: "file_released",
231
+ project_key: "/test/project",
232
+ timestamp: Date.now(),
233
+ agent_name: "BlueLake",
234
+ paths: ["src/auth/**"],
235
+ };
236
+ expect(() => FileReleasedEventSchema.parse(event)).not.toThrow();
237
+ });
238
+
239
+ it("validates file_released with reservation_ids", () => {
240
+ const event = {
241
+ type: "file_released",
242
+ project_key: "/test/project",
243
+ timestamp: Date.now(),
244
+ agent_name: "BlueLake",
245
+ reservation_ids: [1, 2, 3],
246
+ };
247
+ expect(() => FileReleasedEventSchema.parse(event)).not.toThrow();
248
+ });
249
+
250
+ it("validates file_released with neither (release all)", () => {
251
+ const event = {
252
+ type: "file_released",
253
+ project_key: "/test/project",
254
+ timestamp: Date.now(),
255
+ agent_name: "BlueLake",
256
+ };
257
+ expect(() => FileReleasedEventSchema.parse(event)).not.toThrow();
258
+ });
259
+ });
260
+
261
+ describe("TaskStartedEventSchema", () => {
262
+ it("validates task_started event", () => {
263
+ const event = {
264
+ type: "task_started",
265
+ project_key: "/test/project",
266
+ timestamp: Date.now(),
267
+ agent_name: "BlueLake",
268
+ bead_id: "bd-123.1",
269
+ epic_id: "bd-123",
270
+ };
271
+ expect(() => TaskStartedEventSchema.parse(event)).not.toThrow();
272
+ });
273
+ });
274
+
275
+ describe("TaskProgressEventSchema", () => {
276
+ it("validates task_progress event", () => {
277
+ const event = {
278
+ type: "task_progress",
279
+ project_key: "/test/project",
280
+ timestamp: Date.now(),
281
+ agent_name: "BlueLake",
282
+ bead_id: "bd-123.1",
283
+ progress_percent: 50,
284
+ message: "Halfway done",
285
+ files_touched: ["src/auth.ts"],
286
+ };
287
+ expect(() => TaskProgressEventSchema.parse(event)).not.toThrow();
288
+ });
289
+
290
+ it("validates progress_percent bounds", () => {
291
+ const baseEvent = {
292
+ type: "task_progress",
293
+ project_key: "/test/project",
294
+ timestamp: Date.now(),
295
+ agent_name: "BlueLake",
296
+ bead_id: "bd-123.1",
297
+ };
298
+
299
+ // Valid: 0
300
+ expect(() =>
301
+ TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: 0 }),
302
+ ).not.toThrow();
303
+
304
+ // Valid: 100
305
+ expect(() =>
306
+ TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: 100 }),
307
+ ).not.toThrow();
308
+
309
+ // Invalid: -1
310
+ expect(() =>
311
+ TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: -1 }),
312
+ ).toThrow();
313
+
314
+ // Invalid: 101
315
+ expect(() =>
316
+ TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: 101 }),
317
+ ).toThrow();
318
+ });
319
+ });
320
+
321
+ describe("TaskCompletedEventSchema", () => {
322
+ it("validates task_completed event", () => {
323
+ const event = {
324
+ type: "task_completed",
325
+ project_key: "/test/project",
326
+ timestamp: Date.now(),
327
+ agent_name: "BlueLake",
328
+ bead_id: "bd-123.1",
329
+ summary: "Implemented OAuth flow",
330
+ files_touched: ["src/auth.ts", "src/config.ts"],
331
+ success: true,
332
+ };
333
+ expect(() => TaskCompletedEventSchema.parse(event)).not.toThrow();
334
+ });
335
+
336
+ it("defaults success to true", () => {
337
+ const event = {
338
+ type: "task_completed",
339
+ project_key: "/test/project",
340
+ timestamp: Date.now(),
341
+ agent_name: "BlueLake",
342
+ bead_id: "bd-123.1",
343
+ summary: "Done",
344
+ };
345
+ const parsed = TaskCompletedEventSchema.parse(event);
346
+ expect(parsed.success).toBe(true);
347
+ });
348
+ });
349
+
350
+ describe("TaskBlockedEventSchema", () => {
351
+ it("validates task_blocked event", () => {
352
+ const event = {
353
+ type: "task_blocked",
354
+ project_key: "/test/project",
355
+ timestamp: Date.now(),
356
+ agent_name: "BlueLake",
357
+ bead_id: "bd-123.1",
358
+ reason: "Waiting for API credentials",
359
+ };
360
+ expect(() => TaskBlockedEventSchema.parse(event)).not.toThrow();
361
+ });
362
+ });
363
+
364
+ // ============================================================================
365
+ // Discriminated Union Tests
366
+ // ============================================================================
367
+
368
+ describe("AgentEventSchema (discriminated union)", () => {
369
+ it("correctly discriminates by type", () => {
370
+ const events: AgentEvent[] = [
371
+ {
372
+ type: "agent_registered",
373
+ project_key: "/test",
374
+ timestamp: Date.now(),
375
+ agent_name: "Test",
376
+ program: "opencode",
377
+ model: "test",
378
+ },
379
+ {
380
+ type: "agent_active",
381
+ project_key: "/test",
382
+ timestamp: Date.now(),
383
+ agent_name: "Test",
384
+ },
385
+ {
386
+ type: "message_sent",
387
+ project_key: "/test",
388
+ timestamp: Date.now(),
389
+ from_agent: "Test",
390
+ to_agents: ["Other"],
391
+ subject: "Hi",
392
+ body: "Hello",
393
+ importance: "normal",
394
+ ack_required: false,
395
+ },
396
+ ];
397
+
398
+ for (const event of events) {
399
+ expect(() => AgentEventSchema.parse(event)).not.toThrow();
400
+ }
401
+ });
402
+
403
+ it("rejects unknown event types", () => {
404
+ const event = {
405
+ type: "unknown_event",
406
+ project_key: "/test",
407
+ timestamp: Date.now(),
408
+ };
409
+ expect(() => AgentEventSchema.parse(event)).toThrow();
410
+ });
411
+ });
412
+
413
+ // ============================================================================
414
+ // createEvent Helper Tests
415
+ // ============================================================================
416
+
417
+ describe("createEvent", () => {
418
+ it("creates agent_registered event with timestamp", () => {
419
+ const before = Date.now();
420
+ const event = createEvent("agent_registered", {
421
+ project_key: "/test/project",
422
+ agent_name: "BlueLake",
423
+ program: "opencode",
424
+ model: "claude-sonnet-4",
425
+ });
426
+ const after = Date.now();
427
+
428
+ expect(event.type).toBe("agent_registered");
429
+ expect(event.timestamp).toBeGreaterThanOrEqual(before);
430
+ expect(event.timestamp).toBeLessThanOrEqual(after);
431
+ expect(event.agent_name).toBe("BlueLake");
432
+ });
433
+
434
+ it("creates message_sent event", () => {
435
+ const event = createEvent("message_sent", {
436
+ project_key: "/test/project",
437
+ from_agent: "BlueLake",
438
+ to_agents: ["RedStone"],
439
+ subject: "Hello",
440
+ body: "World",
441
+ importance: "high",
442
+ ack_required: true,
443
+ });
444
+
445
+ expect(event.type).toBe("message_sent");
446
+ expect(event.from_agent).toBe("BlueLake");
447
+ expect(event.importance).toBe("high");
448
+ });
449
+
450
+ it("creates file_reserved event", () => {
451
+ const expiresAt = Date.now() + 3600000;
452
+ const event = createEvent("file_reserved", {
453
+ project_key: "/test/project",
454
+ agent_name: "BlueLake",
455
+ paths: ["src/**"],
456
+ exclusive: true,
457
+ ttl_seconds: 3600,
458
+ expires_at: expiresAt,
459
+ });
460
+
461
+ expect(event.type).toBe("file_reserved");
462
+ expect(event.paths).toEqual(["src/**"]);
463
+ expect(event.expires_at).toBe(expiresAt);
464
+ });
465
+
466
+ it("throws on invalid event data", () => {
467
+ expect(() =>
468
+ // @ts-expect-error - intentionally testing invalid data
469
+ createEvent("agent_registered", {
470
+ project_key: "/test/project",
471
+ // Missing agent_name
472
+ }),
473
+ ).toThrow(/Invalid event/);
474
+ });
475
+
476
+ it("throws on invalid event type", () => {
477
+ expect(() =>
478
+ // @ts-expect-error - intentionally testing invalid type
479
+ createEvent("invalid_type", {
480
+ project_key: "/test/project",
481
+ }),
482
+ ).toThrow();
483
+ });
484
+ });
485
+
486
+ // ============================================================================
487
+ // isEventType Type Guard Tests
488
+ // ============================================================================
489
+
490
+ describe("isEventType", () => {
491
+ it("returns true for matching type", () => {
492
+ const event: AgentEvent = {
493
+ type: "agent_registered",
494
+ project_key: "/test",
495
+ timestamp: Date.now(),
496
+ agent_name: "Test",
497
+ program: "opencode",
498
+ model: "test",
499
+ };
500
+
501
+ expect(isEventType(event, "agent_registered")).toBe(true);
502
+ });
503
+
504
+ it("returns false for non-matching type", () => {
505
+ const event: AgentEvent = {
506
+ type: "agent_registered",
507
+ project_key: "/test",
508
+ timestamp: Date.now(),
509
+ agent_name: "Test",
510
+ program: "opencode",
511
+ model: "test",
512
+ };
513
+
514
+ expect(isEventType(event, "agent_active")).toBe(false);
515
+ expect(isEventType(event, "message_sent")).toBe(false);
516
+ });
517
+
518
+ it("narrows type correctly", () => {
519
+ const event: AgentEvent = {
520
+ type: "message_sent",
521
+ project_key: "/test",
522
+ timestamp: Date.now(),
523
+ from_agent: "Test",
524
+ to_agents: ["Other"],
525
+ subject: "Hi",
526
+ body: "Hello",
527
+ importance: "normal",
528
+ ack_required: false,
529
+ };
530
+
531
+ if (isEventType(event, "message_sent")) {
532
+ // TypeScript should know these properties exist
533
+ expect(event.from_agent).toBe("Test");
534
+ expect(event.to_agents).toEqual(["Other"]);
535
+ expect(event.subject).toBe("Hi");
536
+ } else {
537
+ // Should not reach here
538
+ expect(true).toBe(false);
539
+ }
540
+ });
541
+ });
542
+
543
+ // ============================================================================
544
+ // Edge Cases
545
+ // ============================================================================
546
+
547
+ describe("Edge cases", () => {
548
+ it("handles very long strings", () => {
549
+ const longString = "a".repeat(10000);
550
+ const event = createEvent("message_sent", {
551
+ project_key: "/test/project",
552
+ from_agent: "BlueLake",
553
+ to_agents: ["RedStone"],
554
+ subject: longString,
555
+ body: longString,
556
+ importance: "normal",
557
+ ack_required: false,
558
+ });
559
+
560
+ expect(event.subject.length).toBe(10000);
561
+ expect(event.body.length).toBe(10000);
562
+ });
563
+
564
+ it("handles special characters in strings", () => {
565
+ const specialChars = "Hello\n\t\"'\\<>&日本語🎉";
566
+ const event = createEvent("message_sent", {
567
+ project_key: "/test/project",
568
+ from_agent: "BlueLake",
569
+ to_agents: ["RedStone"],
570
+ subject: specialChars,
571
+ body: specialChars,
572
+ importance: "normal",
573
+ ack_required: false,
574
+ });
575
+
576
+ expect(event.subject).toBe(specialChars);
577
+ expect(event.body).toBe(specialChars);
578
+ });
579
+
580
+ it("handles many recipients", () => {
581
+ const manyAgents = Array.from({ length: 100 }, (_, i) => `Agent${i}`);
582
+ const event = createEvent("message_sent", {
583
+ project_key: "/test/project",
584
+ from_agent: "BlueLake",
585
+ to_agents: manyAgents,
586
+ subject: "Broadcast",
587
+ body: "Hello everyone",
588
+ importance: "normal",
589
+ ack_required: false,
590
+ });
591
+
592
+ expect(event.to_agents.length).toBe(100);
593
+ });
594
+
595
+ it("handles many file paths", () => {
596
+ const manyPaths = Array.from({ length: 50 }, (_, i) => `src/file${i}.ts`);
597
+ const event = createEvent("file_reserved", {
598
+ project_key: "/test/project",
599
+ agent_name: "BlueLake",
600
+ paths: manyPaths,
601
+ exclusive: true,
602
+ ttl_seconds: 3600,
603
+ expires_at: Date.now() + 3600000,
604
+ });
605
+
606
+ expect(event.paths.length).toBe(50);
607
+ });
608
+
609
+ it("handles timestamp at epoch", () => {
610
+ const event = {
611
+ type: "agent_active",
612
+ project_key: "/test",
613
+ timestamp: 0,
614
+ agent_name: "Test",
615
+ };
616
+ expect(() => AgentActiveEventSchema.parse(event)).not.toThrow();
617
+ });
618
+
619
+ it("handles very large timestamp", () => {
620
+ const event = {
621
+ type: "agent_active",
622
+ project_key: "/test",
623
+ timestamp: Number.MAX_SAFE_INTEGER,
624
+ agent_name: "Test",
625
+ };
626
+ expect(() => AgentActiveEventSchema.parse(event)).not.toThrow();
627
+ });
628
+ });