swarm-mail 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +28 -0
  2. package/dist/adapter.d.ts +36 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/index.d.ts +22 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +16710 -0
  7. package/{src/pglite.ts → dist/pglite.d.ts} +7 -93
  8. package/dist/pglite.d.ts.map +1 -0
  9. package/dist/streams/agent-mail.d.ts +139 -0
  10. package/dist/streams/agent-mail.d.ts.map +1 -0
  11. package/dist/streams/debug.d.ts +173 -0
  12. package/dist/streams/debug.d.ts.map +1 -0
  13. package/dist/streams/effect/ask.d.ts +124 -0
  14. package/dist/streams/effect/ask.d.ts.map +1 -0
  15. package/dist/streams/effect/cursor.d.ts +87 -0
  16. package/dist/streams/effect/cursor.d.ts.map +1 -0
  17. package/dist/streams/effect/deferred.d.ts +108 -0
  18. package/dist/streams/effect/deferred.d.ts.map +1 -0
  19. package/{src/streams/effect/index.ts → dist/streams/effect/index.d.ts} +1 -0
  20. package/dist/streams/effect/index.d.ts.map +1 -0
  21. package/{src/streams/effect/layers.ts → dist/streams/effect/layers.d.ts} +8 -33
  22. package/dist/streams/effect/layers.d.ts.map +1 -0
  23. package/dist/streams/effect/lock.d.ts +137 -0
  24. package/dist/streams/effect/lock.d.ts.map +1 -0
  25. package/dist/streams/effect/mailbox.d.ts +98 -0
  26. package/dist/streams/effect/mailbox.d.ts.map +1 -0
  27. package/dist/streams/events.d.ts +487 -0
  28. package/dist/streams/events.d.ts.map +1 -0
  29. package/dist/streams/index.d.ts +106 -0
  30. package/dist/streams/index.d.ts.map +1 -0
  31. package/dist/streams/migrations.d.ts +102 -0
  32. package/dist/streams/migrations.d.ts.map +1 -0
  33. package/dist/streams/projections.d.ts +173 -0
  34. package/dist/streams/projections.d.ts.map +1 -0
  35. package/dist/streams/store.d.ts +171 -0
  36. package/dist/streams/store.d.ts.map +1 -0
  37. package/dist/streams/swarm-mail.d.ts +153 -0
  38. package/dist/streams/swarm-mail.d.ts.map +1 -0
  39. package/dist/types/adapter.d.ts +267 -0
  40. package/dist/types/adapter.d.ts.map +1 -0
  41. package/dist/types/database.d.ts +117 -0
  42. package/dist/types/database.d.ts.map +1 -0
  43. package/{src/types/index.ts → dist/types/index.d.ts} +2 -15
  44. package/dist/types/index.d.ts.map +1 -0
  45. package/package.json +21 -5
  46. package/src/adapter.ts +0 -306
  47. package/src/index.ts +0 -57
  48. package/src/streams/agent-mail.test.ts +0 -777
  49. package/src/streams/agent-mail.ts +0 -535
  50. package/src/streams/debug.test.ts +0 -500
  51. package/src/streams/debug.ts +0 -727
  52. package/src/streams/effect/ask.integration.test.ts +0 -314
  53. package/src/streams/effect/ask.ts +0 -202
  54. package/src/streams/effect/cursor.integration.test.ts +0 -418
  55. package/src/streams/effect/cursor.ts +0 -288
  56. package/src/streams/effect/deferred.test.ts +0 -357
  57. package/src/streams/effect/deferred.ts +0 -445
  58. package/src/streams/effect/lock.test.ts +0 -385
  59. package/src/streams/effect/lock.ts +0 -399
  60. package/src/streams/effect/mailbox.test.ts +0 -260
  61. package/src/streams/effect/mailbox.ts +0 -318
  62. package/src/streams/events.test.ts +0 -924
  63. package/src/streams/events.ts +0 -329
  64. package/src/streams/index.test.ts +0 -229
  65. package/src/streams/index.ts +0 -578
  66. package/src/streams/migrations.test.ts +0 -359
  67. package/src/streams/migrations.ts +0 -362
  68. package/src/streams/projections.test.ts +0 -611
  69. package/src/streams/projections.ts +0 -564
  70. package/src/streams/store.integration.test.ts +0 -658
  71. package/src/streams/store.ts +0 -1129
  72. package/src/streams/swarm-mail.ts +0 -552
  73. package/src/types/adapter.ts +0 -392
  74. package/src/types/database.ts +0 -127
  75. package/tsconfig.json +0 -22
@@ -1,924 +0,0 @@
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
- DecompositionGeneratedEventSchema,
25
- SubtaskOutcomeEventSchema,
26
- HumanFeedbackEventSchema,
27
- createEvent,
28
- isEventType,
29
- type AgentEvent,
30
- } from "./events";
31
-
32
- // ============================================================================
33
- // Schema Validation Tests
34
- // ============================================================================
35
-
36
- describe("AgentRegisteredEventSchema", () => {
37
- it("validates a complete agent_registered event", () => {
38
- const event = {
39
- type: "agent_registered",
40
- project_key: "/test/project",
41
- timestamp: Date.now(),
42
- agent_name: "BlueLake",
43
- program: "opencode",
44
- model: "claude-sonnet-4",
45
- task_description: "Working on auth",
46
- };
47
- expect(() => AgentRegisteredEventSchema.parse(event)).not.toThrow();
48
- });
49
-
50
- it("applies defaults for program and model", () => {
51
- const event = {
52
- type: "agent_registered",
53
- project_key: "/test/project",
54
- timestamp: Date.now(),
55
- agent_name: "BlueLake",
56
- };
57
- const parsed = AgentRegisteredEventSchema.parse(event);
58
- expect(parsed.program).toBe("opencode");
59
- expect(parsed.model).toBe("unknown");
60
- });
61
-
62
- it("rejects missing agent_name", () => {
63
- const event = {
64
- type: "agent_registered",
65
- project_key: "/test/project",
66
- timestamp: Date.now(),
67
- };
68
- expect(() => AgentRegisteredEventSchema.parse(event)).toThrow();
69
- });
70
- });
71
-
72
- describe("AgentActiveEventSchema", () => {
73
- it("validates agent_active event", () => {
74
- const event = {
75
- type: "agent_active",
76
- project_key: "/test/project",
77
- timestamp: Date.now(),
78
- agent_name: "BlueLake",
79
- };
80
- expect(() => AgentActiveEventSchema.parse(event)).not.toThrow();
81
- });
82
- });
83
-
84
- describe("MessageSentEventSchema", () => {
85
- it("validates a complete message_sent event", () => {
86
- const event = {
87
- type: "message_sent",
88
- project_key: "/test/project",
89
- timestamp: Date.now(),
90
- from_agent: "BlueLake",
91
- to_agents: ["RedStone", "GreenCastle"],
92
- subject: "Task update",
93
- body: "Completed the auth module",
94
- thread_id: "bd-123",
95
- importance: "high",
96
- ack_required: true,
97
- };
98
- expect(() => MessageSentEventSchema.parse(event)).not.toThrow();
99
- });
100
-
101
- it("applies defaults for importance and ack_required", () => {
102
- const event = {
103
- type: "message_sent",
104
- project_key: "/test/project",
105
- timestamp: Date.now(),
106
- from_agent: "BlueLake",
107
- to_agents: ["RedStone"],
108
- subject: "Hello",
109
- body: "World",
110
- };
111
- const parsed = MessageSentEventSchema.parse(event);
112
- expect(parsed.importance).toBe("normal");
113
- expect(parsed.ack_required).toBe(false);
114
- });
115
-
116
- it("validates importance enum values", () => {
117
- const validImportance = ["low", "normal", "high", "urgent"];
118
- for (const importance of validImportance) {
119
- const event = {
120
- type: "message_sent",
121
- project_key: "/test/project",
122
- timestamp: Date.now(),
123
- from_agent: "BlueLake",
124
- to_agents: ["RedStone"],
125
- subject: "Test",
126
- body: "Test",
127
- importance,
128
- };
129
- expect(() => MessageSentEventSchema.parse(event)).not.toThrow();
130
- }
131
- });
132
-
133
- it("rejects invalid importance value", () => {
134
- const event = {
135
- type: "message_sent",
136
- project_key: "/test/project",
137
- timestamp: Date.now(),
138
- from_agent: "BlueLake",
139
- to_agents: ["RedStone"],
140
- subject: "Test",
141
- body: "Test",
142
- importance: "critical", // Invalid
143
- };
144
- expect(() => MessageSentEventSchema.parse(event)).toThrow();
145
- });
146
-
147
- it("rejects empty to_agents array", () => {
148
- const event = {
149
- type: "message_sent",
150
- project_key: "/test/project",
151
- timestamp: Date.now(),
152
- from_agent: "BlueLake",
153
- to_agents: [],
154
- subject: "Test",
155
- body: "Test",
156
- };
157
- // Empty array is technically valid per schema - it's a broadcast
158
- expect(() => MessageSentEventSchema.parse(event)).not.toThrow();
159
- });
160
- });
161
-
162
- describe("MessageReadEventSchema", () => {
163
- it("validates message_read event", () => {
164
- const event = {
165
- type: "message_read",
166
- project_key: "/test/project",
167
- timestamp: Date.now(),
168
- message_id: 42,
169
- agent_name: "RedStone",
170
- };
171
- expect(() => MessageReadEventSchema.parse(event)).not.toThrow();
172
- });
173
- });
174
-
175
- describe("MessageAckedEventSchema", () => {
176
- it("validates message_acked event", () => {
177
- const event = {
178
- type: "message_acked",
179
- project_key: "/test/project",
180
- timestamp: Date.now(),
181
- message_id: 42,
182
- agent_name: "RedStone",
183
- };
184
- expect(() => MessageAckedEventSchema.parse(event)).not.toThrow();
185
- });
186
- });
187
-
188
- describe("FileReservedEventSchema", () => {
189
- it("validates a complete file_reserved event", () => {
190
- const event = {
191
- type: "file_reserved",
192
- project_key: "/test/project",
193
- timestamp: Date.now(),
194
- agent_name: "BlueLake",
195
- paths: ["src/auth/**", "src/config.ts"],
196
- reason: "bd-123: Working on auth",
197
- exclusive: true,
198
- ttl_seconds: 3600,
199
- expires_at: Date.now() + 3600000,
200
- };
201
- expect(() => FileReservedEventSchema.parse(event)).not.toThrow();
202
- });
203
-
204
- it("applies defaults for exclusive and ttl_seconds", () => {
205
- const event = {
206
- type: "file_reserved",
207
- project_key: "/test/project",
208
- timestamp: Date.now(),
209
- agent_name: "BlueLake",
210
- paths: ["src/auth/**"],
211
- expires_at: Date.now() + 3600000,
212
- };
213
- const parsed = FileReservedEventSchema.parse(event);
214
- expect(parsed.exclusive).toBe(true);
215
- expect(parsed.ttl_seconds).toBe(3600);
216
- });
217
-
218
- it("requires expires_at", () => {
219
- const event = {
220
- type: "file_reserved",
221
- project_key: "/test/project",
222
- timestamp: Date.now(),
223
- agent_name: "BlueLake",
224
- paths: ["src/auth/**"],
225
- };
226
- expect(() => FileReservedEventSchema.parse(event)).toThrow();
227
- });
228
- });
229
-
230
- describe("FileReleasedEventSchema", () => {
231
- it("validates file_released with paths", () => {
232
- const event = {
233
- type: "file_released",
234
- project_key: "/test/project",
235
- timestamp: Date.now(),
236
- agent_name: "BlueLake",
237
- paths: ["src/auth/**"],
238
- };
239
- expect(() => FileReleasedEventSchema.parse(event)).not.toThrow();
240
- });
241
-
242
- it("validates file_released with reservation_ids", () => {
243
- const event = {
244
- type: "file_released",
245
- project_key: "/test/project",
246
- timestamp: Date.now(),
247
- agent_name: "BlueLake",
248
- reservation_ids: [1, 2, 3],
249
- };
250
- expect(() => FileReleasedEventSchema.parse(event)).not.toThrow();
251
- });
252
-
253
- it("validates file_released with neither (release all)", () => {
254
- const event = {
255
- type: "file_released",
256
- project_key: "/test/project",
257
- timestamp: Date.now(),
258
- agent_name: "BlueLake",
259
- };
260
- expect(() => FileReleasedEventSchema.parse(event)).not.toThrow();
261
- });
262
- });
263
-
264
- describe("TaskStartedEventSchema", () => {
265
- it("validates task_started event", () => {
266
- const event = {
267
- type: "task_started",
268
- project_key: "/test/project",
269
- timestamp: Date.now(),
270
- agent_name: "BlueLake",
271
- bead_id: "bd-123.1",
272
- epic_id: "bd-123",
273
- };
274
- expect(() => TaskStartedEventSchema.parse(event)).not.toThrow();
275
- });
276
- });
277
-
278
- describe("TaskProgressEventSchema", () => {
279
- it("validates task_progress event", () => {
280
- const event = {
281
- type: "task_progress",
282
- project_key: "/test/project",
283
- timestamp: Date.now(),
284
- agent_name: "BlueLake",
285
- bead_id: "bd-123.1",
286
- progress_percent: 50,
287
- message: "Halfway done",
288
- files_touched: ["src/auth.ts"],
289
- };
290
- expect(() => TaskProgressEventSchema.parse(event)).not.toThrow();
291
- });
292
-
293
- it("validates progress_percent bounds", () => {
294
- const baseEvent = {
295
- type: "task_progress",
296
- project_key: "/test/project",
297
- timestamp: Date.now(),
298
- agent_name: "BlueLake",
299
- bead_id: "bd-123.1",
300
- };
301
-
302
- // Valid: 0
303
- expect(() =>
304
- TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: 0 }),
305
- ).not.toThrow();
306
-
307
- // Valid: 100
308
- expect(() =>
309
- TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: 100 }),
310
- ).not.toThrow();
311
-
312
- // Invalid: -1
313
- expect(() =>
314
- TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: -1 }),
315
- ).toThrow();
316
-
317
- // Invalid: 101
318
- expect(() =>
319
- TaskProgressEventSchema.parse({ ...baseEvent, progress_percent: 101 }),
320
- ).toThrow();
321
- });
322
- });
323
-
324
- describe("TaskCompletedEventSchema", () => {
325
- it("validates task_completed event", () => {
326
- const event = {
327
- type: "task_completed",
328
- project_key: "/test/project",
329
- timestamp: Date.now(),
330
- agent_name: "BlueLake",
331
- bead_id: "bd-123.1",
332
- summary: "Implemented OAuth flow",
333
- files_touched: ["src/auth.ts", "src/config.ts"],
334
- success: true,
335
- };
336
- expect(() => TaskCompletedEventSchema.parse(event)).not.toThrow();
337
- });
338
-
339
- it("defaults success to true", () => {
340
- const event = {
341
- type: "task_completed",
342
- project_key: "/test/project",
343
- timestamp: Date.now(),
344
- agent_name: "BlueLake",
345
- bead_id: "bd-123.1",
346
- summary: "Done",
347
- };
348
- const parsed = TaskCompletedEventSchema.parse(event);
349
- expect(parsed.success).toBe(true);
350
- });
351
- });
352
-
353
- describe("TaskBlockedEventSchema", () => {
354
- it("validates task_blocked event", () => {
355
- const event = {
356
- type: "task_blocked",
357
- project_key: "/test/project",
358
- timestamp: Date.now(),
359
- agent_name: "BlueLake",
360
- bead_id: "bd-123.1",
361
- reason: "Waiting for API credentials",
362
- };
363
- expect(() => TaskBlockedEventSchema.parse(event)).not.toThrow();
364
- });
365
- });
366
-
367
- describe("DecompositionGeneratedEventSchema", () => {
368
- it("validates a complete decomposition_generated event", () => {
369
- const event = {
370
- type: "decomposition_generated",
371
- project_key: "/test/project",
372
- timestamp: Date.now(),
373
- epic_id: "bd-123",
374
- task: "Add user authentication",
375
- context: "OAuth integration for GitHub",
376
- strategy: "feature-based",
377
- epic_title: "User Authentication",
378
- subtasks: [
379
- {
380
- title: "Create OAuth flow",
381
- files: ["src/auth/oauth.ts"],
382
- priority: 2,
383
- },
384
- { title: "Add login UI", files: ["src/ui/login.tsx"], priority: 1 },
385
- ],
386
- };
387
- expect(() => DecompositionGeneratedEventSchema.parse(event)).not.toThrow();
388
- });
389
-
390
- it("validates without optional context", () => {
391
- const event = {
392
- type: "decomposition_generated",
393
- project_key: "/test/project",
394
- timestamp: Date.now(),
395
- epic_id: "bd-123",
396
- task: "Add user authentication",
397
- strategy: "file-based",
398
- epic_title: "User Authentication",
399
- subtasks: [{ title: "Create OAuth flow", files: ["src/auth/oauth.ts"] }],
400
- };
401
- expect(() => DecompositionGeneratedEventSchema.parse(event)).not.toThrow();
402
- });
403
-
404
- it("validates strategy enum values", () => {
405
- const validStrategies = ["file-based", "feature-based", "risk-based"];
406
- for (const strategy of validStrategies) {
407
- const event = {
408
- type: "decomposition_generated",
409
- project_key: "/test/project",
410
- timestamp: Date.now(),
411
- epic_id: "bd-123",
412
- task: "Test task",
413
- strategy,
414
- epic_title: "Test",
415
- subtasks: [{ title: "Subtask", files: ["test.ts"] }],
416
- };
417
- expect(() =>
418
- DecompositionGeneratedEventSchema.parse(event),
419
- ).not.toThrow();
420
- }
421
- });
422
-
423
- it("rejects invalid strategy value", () => {
424
- const event = {
425
- type: "decomposition_generated",
426
- project_key: "/test/project",
427
- timestamp: Date.now(),
428
- epic_id: "bd-123",
429
- task: "Test task",
430
- strategy: "invalid-strategy",
431
- epic_title: "Test",
432
- subtasks: [{ title: "Subtask", files: ["test.ts"] }],
433
- };
434
- expect(() => DecompositionGeneratedEventSchema.parse(event)).toThrow();
435
- });
436
-
437
- it("validates subtask priority bounds", () => {
438
- const baseEvent = {
439
- type: "decomposition_generated",
440
- project_key: "/test/project",
441
- timestamp: Date.now(),
442
- epic_id: "bd-123",
443
- task: "Test",
444
- strategy: "file-based",
445
- epic_title: "Test",
446
- };
447
-
448
- // Valid: 0
449
- expect(() =>
450
- DecompositionGeneratedEventSchema.parse({
451
- ...baseEvent,
452
- subtasks: [{ title: "Test", files: ["test.ts"], priority: 0 }],
453
- }),
454
- ).not.toThrow();
455
-
456
- // Valid: 3
457
- expect(() =>
458
- DecompositionGeneratedEventSchema.parse({
459
- ...baseEvent,
460
- subtasks: [{ title: "Test", files: ["test.ts"], priority: 3 }],
461
- }),
462
- ).not.toThrow();
463
-
464
- // Invalid: -1
465
- expect(() =>
466
- DecompositionGeneratedEventSchema.parse({
467
- ...baseEvent,
468
- subtasks: [{ title: "Test", files: ["test.ts"], priority: -1 }],
469
- }),
470
- ).toThrow();
471
-
472
- // Invalid: 4
473
- expect(() =>
474
- DecompositionGeneratedEventSchema.parse({
475
- ...baseEvent,
476
- subtasks: [{ title: "Test", files: ["test.ts"], priority: 4 }],
477
- }),
478
- ).toThrow();
479
- });
480
-
481
- it("rejects empty subtasks array", () => {
482
- const event = {
483
- type: "decomposition_generated",
484
- project_key: "/test/project",
485
- timestamp: Date.now(),
486
- epic_id: "bd-123",
487
- task: "Test",
488
- strategy: "file-based",
489
- epic_title: "Test",
490
- subtasks: [],
491
- };
492
- // Empty subtasks is valid per schema but semantically questionable
493
- expect(() => DecompositionGeneratedEventSchema.parse(event)).not.toThrow();
494
- });
495
- });
496
-
497
- describe("SubtaskOutcomeEventSchema", () => {
498
- it("validates a complete subtask_outcome event", () => {
499
- const event = {
500
- type: "subtask_outcome",
501
- project_key: "/test/project",
502
- timestamp: Date.now(),
503
- epic_id: "bd-123",
504
- bead_id: "bd-123.1",
505
- planned_files: ["src/auth.ts", "src/config.ts"],
506
- actual_files: ["src/auth.ts", "src/config.ts", "src/utils.ts"],
507
- duration_ms: 45000,
508
- error_count: 2,
509
- retry_count: 1,
510
- success: true,
511
- };
512
- expect(() => SubtaskOutcomeEventSchema.parse(event)).not.toThrow();
513
- });
514
-
515
- it("applies defaults for error_count and retry_count", () => {
516
- const event = {
517
- type: "subtask_outcome",
518
- project_key: "/test/project",
519
- timestamp: Date.now(),
520
- epic_id: "bd-123",
521
- bead_id: "bd-123.1",
522
- planned_files: ["src/auth.ts"],
523
- actual_files: ["src/auth.ts"],
524
- duration_ms: 10000,
525
- success: true,
526
- };
527
- const parsed = SubtaskOutcomeEventSchema.parse(event);
528
- expect(parsed.error_count).toBe(0);
529
- expect(parsed.retry_count).toBe(0);
530
- });
531
-
532
- it("validates duration_ms is non-negative", () => {
533
- const baseEvent = {
534
- type: "subtask_outcome",
535
- project_key: "/test/project",
536
- timestamp: Date.now(),
537
- epic_id: "bd-123",
538
- bead_id: "bd-123.1",
539
- planned_files: ["test.ts"],
540
- actual_files: ["test.ts"],
541
- success: true,
542
- };
543
-
544
- // Valid: 0
545
- expect(() =>
546
- SubtaskOutcomeEventSchema.parse({ ...baseEvent, duration_ms: 0 }),
547
- ).not.toThrow();
548
-
549
- // Valid: positive
550
- expect(() =>
551
- SubtaskOutcomeEventSchema.parse({ ...baseEvent, duration_ms: 1000 }),
552
- ).not.toThrow();
553
-
554
- // Invalid: negative
555
- expect(() =>
556
- SubtaskOutcomeEventSchema.parse({ ...baseEvent, duration_ms: -1 }),
557
- ).toThrow();
558
- });
559
-
560
- it("validates error_count is non-negative", () => {
561
- const baseEvent = {
562
- type: "subtask_outcome",
563
- project_key: "/test/project",
564
- timestamp: Date.now(),
565
- epic_id: "bd-123",
566
- bead_id: "bd-123.1",
567
- planned_files: ["test.ts"],
568
- actual_files: ["test.ts"],
569
- duration_ms: 1000,
570
- success: true,
571
- };
572
-
573
- // Invalid: negative
574
- expect(() =>
575
- SubtaskOutcomeEventSchema.parse({ ...baseEvent, error_count: -1 }),
576
- ).toThrow();
577
- });
578
-
579
- it("handles file lists with different lengths", () => {
580
- const event = {
581
- type: "subtask_outcome",
582
- project_key: "/test/project",
583
- timestamp: Date.now(),
584
- epic_id: "bd-123",
585
- bead_id: "bd-123.1",
586
- planned_files: ["a.ts", "b.ts"],
587
- actual_files: ["a.ts", "b.ts", "c.ts", "d.ts"],
588
- duration_ms: 5000,
589
- success: true,
590
- };
591
- expect(() => SubtaskOutcomeEventSchema.parse(event)).not.toThrow();
592
- });
593
- });
594
-
595
- describe("HumanFeedbackEventSchema", () => {
596
- it("validates a complete human_feedback event", () => {
597
- const event = {
598
- type: "human_feedback",
599
- project_key: "/test/project",
600
- timestamp: Date.now(),
601
- epic_id: "bd-123",
602
- accepted: true,
603
- modified: false,
604
- notes: "Looks good, no changes needed",
605
- };
606
- expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
607
- });
608
-
609
- it("validates accepted with modification", () => {
610
- const event = {
611
- type: "human_feedback",
612
- project_key: "/test/project",
613
- timestamp: Date.now(),
614
- epic_id: "bd-123",
615
- accepted: true,
616
- modified: true,
617
- notes: "Changed priority on subtask 2",
618
- };
619
- expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
620
- });
621
-
622
- it("validates rejected feedback", () => {
623
- const event = {
624
- type: "human_feedback",
625
- project_key: "/test/project",
626
- timestamp: Date.now(),
627
- epic_id: "bd-123",
628
- accepted: false,
629
- modified: false,
630
- notes: "Decomposition too granular, needs consolidation",
631
- };
632
- expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
633
- });
634
-
635
- it("applies default for modified", () => {
636
- const event = {
637
- type: "human_feedback",
638
- project_key: "/test/project",
639
- timestamp: Date.now(),
640
- epic_id: "bd-123",
641
- accepted: true,
642
- };
643
- const parsed = HumanFeedbackEventSchema.parse(event);
644
- expect(parsed.modified).toBe(false);
645
- });
646
-
647
- it("validates without notes", () => {
648
- const event = {
649
- type: "human_feedback",
650
- project_key: "/test/project",
651
- timestamp: Date.now(),
652
- epic_id: "bd-123",
653
- accepted: true,
654
- modified: false,
655
- };
656
- expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
657
- });
658
- });
659
-
660
- // ============================================================================
661
- // Discriminated Union Tests
662
- // ============================================================================
663
-
664
- describe("AgentEventSchema (discriminated union)", () => {
665
- it("correctly discriminates by type", () => {
666
- const events: AgentEvent[] = [
667
- {
668
- type: "agent_registered",
669
- project_key: "/test",
670
- timestamp: Date.now(),
671
- agent_name: "Test",
672
- program: "opencode",
673
- model: "test",
674
- },
675
- {
676
- type: "agent_active",
677
- project_key: "/test",
678
- timestamp: Date.now(),
679
- agent_name: "Test",
680
- },
681
- {
682
- type: "message_sent",
683
- project_key: "/test",
684
- timestamp: Date.now(),
685
- from_agent: "Test",
686
- to_agents: ["Other"],
687
- subject: "Hi",
688
- body: "Hello",
689
- importance: "normal",
690
- ack_required: false,
691
- },
692
- ];
693
-
694
- for (const event of events) {
695
- expect(() => AgentEventSchema.parse(event)).not.toThrow();
696
- }
697
- });
698
-
699
- it("rejects unknown event types", () => {
700
- const event = {
701
- type: "unknown_event",
702
- project_key: "/test",
703
- timestamp: Date.now(),
704
- };
705
- expect(() => AgentEventSchema.parse(event)).toThrow();
706
- });
707
- });
708
-
709
- // ============================================================================
710
- // createEvent Helper Tests
711
- // ============================================================================
712
-
713
- describe("createEvent", () => {
714
- it("creates agent_registered event with timestamp", () => {
715
- const before = Date.now();
716
- const event = createEvent("agent_registered", {
717
- project_key: "/test/project",
718
- agent_name: "BlueLake",
719
- program: "opencode",
720
- model: "claude-sonnet-4",
721
- });
722
- const after = Date.now();
723
-
724
- expect(event.type).toBe("agent_registered");
725
- expect(event.timestamp).toBeGreaterThanOrEqual(before);
726
- expect(event.timestamp).toBeLessThanOrEqual(after);
727
- expect(event.agent_name).toBe("BlueLake");
728
- });
729
-
730
- it("creates message_sent event", () => {
731
- const event = createEvent("message_sent", {
732
- project_key: "/test/project",
733
- from_agent: "BlueLake",
734
- to_agents: ["RedStone"],
735
- subject: "Hello",
736
- body: "World",
737
- importance: "high",
738
- ack_required: true,
739
- });
740
-
741
- expect(event.type).toBe("message_sent");
742
- expect(event.from_agent).toBe("BlueLake");
743
- expect(event.importance).toBe("high");
744
- });
745
-
746
- it("creates file_reserved event", () => {
747
- const expiresAt = Date.now() + 3600000;
748
- const event = createEvent("file_reserved", {
749
- project_key: "/test/project",
750
- agent_name: "BlueLake",
751
- paths: ["src/**"],
752
- exclusive: true,
753
- ttl_seconds: 3600,
754
- expires_at: expiresAt,
755
- });
756
-
757
- expect(event.type).toBe("file_reserved");
758
- expect(event.paths).toEqual(["src/**"]);
759
- expect(event.expires_at).toBe(expiresAt);
760
- });
761
-
762
- it("throws on invalid event data", () => {
763
- expect(() =>
764
- // @ts-expect-error - intentionally testing invalid data
765
- createEvent("agent_registered", {
766
- project_key: "/test/project",
767
- // Missing agent_name
768
- }),
769
- ).toThrow(/Invalid event/);
770
- });
771
-
772
- it("throws on invalid event type", () => {
773
- expect(() =>
774
- // @ts-expect-error - intentionally testing invalid type
775
- createEvent("invalid_type", {
776
- project_key: "/test/project",
777
- }),
778
- ).toThrow();
779
- });
780
- });
781
-
782
- // ============================================================================
783
- // isEventType Type Guard Tests
784
- // ============================================================================
785
-
786
- describe("isEventType", () => {
787
- it("returns true for matching type", () => {
788
- const event: AgentEvent = {
789
- type: "agent_registered",
790
- project_key: "/test",
791
- timestamp: Date.now(),
792
- agent_name: "Test",
793
- program: "opencode",
794
- model: "test",
795
- };
796
-
797
- expect(isEventType(event, "agent_registered")).toBe(true);
798
- });
799
-
800
- it("returns false for non-matching type", () => {
801
- const event: AgentEvent = {
802
- type: "agent_registered",
803
- project_key: "/test",
804
- timestamp: Date.now(),
805
- agent_name: "Test",
806
- program: "opencode",
807
- model: "test",
808
- };
809
-
810
- expect(isEventType(event, "agent_active")).toBe(false);
811
- expect(isEventType(event, "message_sent")).toBe(false);
812
- });
813
-
814
- it("narrows type correctly", () => {
815
- const event: AgentEvent = {
816
- type: "message_sent",
817
- project_key: "/test",
818
- timestamp: Date.now(),
819
- from_agent: "Test",
820
- to_agents: ["Other"],
821
- subject: "Hi",
822
- body: "Hello",
823
- importance: "normal",
824
- ack_required: false,
825
- };
826
-
827
- if (isEventType(event, "message_sent")) {
828
- // TypeScript should know these properties exist
829
- expect(event.from_agent).toBe("Test");
830
- expect(event.to_agents).toEqual(["Other"]);
831
- expect(event.subject).toBe("Hi");
832
- } else {
833
- // Should not reach here
834
- expect(true).toBe(false);
835
- }
836
- });
837
- });
838
-
839
- // ============================================================================
840
- // Edge Cases
841
- // ============================================================================
842
-
843
- describe("Edge cases", () => {
844
- it("handles very long strings", () => {
845
- const longString = "a".repeat(10000);
846
- const event = createEvent("message_sent", {
847
- project_key: "/test/project",
848
- from_agent: "BlueLake",
849
- to_agents: ["RedStone"],
850
- subject: longString,
851
- body: longString,
852
- importance: "normal",
853
- ack_required: false,
854
- });
855
-
856
- expect(event.subject.length).toBe(10000);
857
- expect(event.body.length).toBe(10000);
858
- });
859
-
860
- it("handles special characters in strings", () => {
861
- const specialChars = "Hello\n\t\"'\\<>&日本語🎉";
862
- const event = createEvent("message_sent", {
863
- project_key: "/test/project",
864
- from_agent: "BlueLake",
865
- to_agents: ["RedStone"],
866
- subject: specialChars,
867
- body: specialChars,
868
- importance: "normal",
869
- ack_required: false,
870
- });
871
-
872
- expect(event.subject).toBe(specialChars);
873
- expect(event.body).toBe(specialChars);
874
- });
875
-
876
- it("handles many recipients", () => {
877
- const manyAgents = Array.from({ length: 100 }, (_, i) => `Agent${i}`);
878
- const event = createEvent("message_sent", {
879
- project_key: "/test/project",
880
- from_agent: "BlueLake",
881
- to_agents: manyAgents,
882
- subject: "Broadcast",
883
- body: "Hello everyone",
884
- importance: "normal",
885
- ack_required: false,
886
- });
887
-
888
- expect(event.to_agents.length).toBe(100);
889
- });
890
-
891
- it("handles many file paths", () => {
892
- const manyPaths = Array.from({ length: 50 }, (_, i) => `src/file${i}.ts`);
893
- const event = createEvent("file_reserved", {
894
- project_key: "/test/project",
895
- agent_name: "BlueLake",
896
- paths: manyPaths,
897
- exclusive: true,
898
- ttl_seconds: 3600,
899
- expires_at: Date.now() + 3600000,
900
- });
901
-
902
- expect(event.paths.length).toBe(50);
903
- });
904
-
905
- it("handles timestamp at epoch", () => {
906
- const event = {
907
- type: "agent_active",
908
- project_key: "/test",
909
- timestamp: 0,
910
- agent_name: "Test",
911
- };
912
- expect(() => AgentActiveEventSchema.parse(event)).not.toThrow();
913
- });
914
-
915
- it("handles very large timestamp", () => {
916
- const event = {
917
- type: "agent_active",
918
- project_key: "/test",
919
- timestamp: Number.MAX_SAFE_INTEGER,
920
- agent_name: "Test",
921
- };
922
- expect(() => AgentActiveEventSchema.parse(event)).not.toThrow();
923
- });
924
- });