swarm-mail 0.1.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 (36) hide show
  1. package/README.md +201 -0
  2. package/package.json +28 -0
  3. package/src/adapter.ts +306 -0
  4. package/src/index.ts +57 -0
  5. package/src/pglite.ts +189 -0
  6. package/src/streams/agent-mail.test.ts +777 -0
  7. package/src/streams/agent-mail.ts +535 -0
  8. package/src/streams/debug.test.ts +500 -0
  9. package/src/streams/debug.ts +727 -0
  10. package/src/streams/effect/ask.integration.test.ts +314 -0
  11. package/src/streams/effect/ask.ts +202 -0
  12. package/src/streams/effect/cursor.integration.test.ts +418 -0
  13. package/src/streams/effect/cursor.ts +288 -0
  14. package/src/streams/effect/deferred.test.ts +357 -0
  15. package/src/streams/effect/deferred.ts +445 -0
  16. package/src/streams/effect/index.ts +17 -0
  17. package/src/streams/effect/layers.ts +73 -0
  18. package/src/streams/effect/lock.test.ts +385 -0
  19. package/src/streams/effect/lock.ts +399 -0
  20. package/src/streams/effect/mailbox.test.ts +260 -0
  21. package/src/streams/effect/mailbox.ts +318 -0
  22. package/src/streams/events.test.ts +924 -0
  23. package/src/streams/events.ts +329 -0
  24. package/src/streams/index.test.ts +229 -0
  25. package/src/streams/index.ts +578 -0
  26. package/src/streams/migrations.test.ts +359 -0
  27. package/src/streams/migrations.ts +362 -0
  28. package/src/streams/projections.test.ts +611 -0
  29. package/src/streams/projections.ts +564 -0
  30. package/src/streams/store.integration.test.ts +658 -0
  31. package/src/streams/store.ts +1129 -0
  32. package/src/streams/swarm-mail.ts +552 -0
  33. package/src/types/adapter.ts +392 -0
  34. package/src/types/database.ts +127 -0
  35. package/src/types/index.ts +26 -0
  36. package/tsconfig.json +22 -0
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Debug Tools Tests - TDD RED phase
3
+ *
4
+ * These tests define the expected behavior for debug/inspection tools.
5
+ * Run these first to see them fail, then implement to make them pass.
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import { resetDatabase } from "./index";
9
+ import { initAgent, sendAgentMessage, reserveAgentFiles } from "./agent-mail";
10
+ import {
11
+ debugEvents,
12
+ debugAgent,
13
+ debugMessage,
14
+ debugReservations,
15
+ getEventTimeline,
16
+ inspectState,
17
+ } from "./debug";
18
+
19
+ describe("Debug Tools", () => {
20
+ const projectPath = "/test/debug-project";
21
+
22
+ beforeEach(async () => {
23
+ await resetDatabase(projectPath);
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await resetDatabase(projectPath);
28
+ });
29
+
30
+ // ============================================================================
31
+ // debugEvents - Show recent events with filtering
32
+ // ============================================================================
33
+
34
+ describe("debugEvents", () => {
35
+ it("returns recent events in reverse chronological order", async () => {
36
+ // Setup: create some events
37
+ await initAgent({ projectPath, agentName: "Agent1" });
38
+ await initAgent({ projectPath, agentName: "Agent2" });
39
+
40
+ const result = await debugEvents({ projectPath });
41
+
42
+ expect(result.events).toHaveLength(2);
43
+ expect(result.events[0].type).toBe("agent_registered");
44
+ // Most recent first
45
+ expect(result.events[0].agent_name).toBe("Agent2");
46
+ expect(result.events[1].agent_name).toBe("Agent1");
47
+ });
48
+
49
+ it("filters by event type", async () => {
50
+ await initAgent({ projectPath, agentName: "Agent1" });
51
+ await sendAgentMessage({
52
+ projectPath,
53
+ fromAgent: "Agent1",
54
+ toAgents: ["Agent2"],
55
+ subject: "Test",
56
+ body: "Hello",
57
+ });
58
+
59
+ const result = await debugEvents({
60
+ projectPath,
61
+ types: ["message_sent"],
62
+ });
63
+
64
+ expect(result.events).toHaveLength(1);
65
+ expect(result.events[0].type).toBe("message_sent");
66
+ });
67
+
68
+ it("filters by agent name", async () => {
69
+ await initAgent({ projectPath, agentName: "Agent1" });
70
+ await initAgent({ projectPath, agentName: "Agent2" });
71
+ await sendAgentMessage({
72
+ projectPath,
73
+ fromAgent: "Agent1",
74
+ toAgents: ["Agent2"],
75
+ subject: "Test",
76
+ body: "Hello",
77
+ });
78
+
79
+ const result = await debugEvents({
80
+ projectPath,
81
+ agentName: "Agent1",
82
+ });
83
+
84
+ // Should include Agent1's registration and message
85
+ expect(result.events.length).toBeGreaterThanOrEqual(2);
86
+ expect(
87
+ result.events.every(
88
+ (e) =>
89
+ e.agent_name === "Agent1" ||
90
+ e.from_agent === "Agent1" ||
91
+ e.to_agents?.includes("Agent1"),
92
+ ),
93
+ ).toBe(true);
94
+ });
95
+
96
+ it("limits results", async () => {
97
+ await initAgent({ projectPath, agentName: "Agent1" });
98
+ await initAgent({ projectPath, agentName: "Agent2" });
99
+ await initAgent({ projectPath, agentName: "Agent3" });
100
+
101
+ const result = await debugEvents({ projectPath, limit: 2 });
102
+
103
+ expect(result.events).toHaveLength(2);
104
+ expect(result.total).toBe(3);
105
+ });
106
+
107
+ it("includes human-readable timestamps", async () => {
108
+ await initAgent({ projectPath, agentName: "Agent1" });
109
+
110
+ const result = await debugEvents({ projectPath });
111
+
112
+ expect(result.events[0]).toHaveProperty("timestamp_human");
113
+ expect(typeof result.events[0].timestamp_human).toBe("string");
114
+ // Should be ISO format or similar
115
+ expect(result.events[0].timestamp_human).toMatch(/\d{4}-\d{2}-\d{2}/);
116
+ });
117
+ });
118
+
119
+ // ============================================================================
120
+ // debugAgent - Detailed agent state dump
121
+ // ============================================================================
122
+
123
+ describe("debugAgent", () => {
124
+ it("returns agent details with activity summary", async () => {
125
+ await initAgent({
126
+ projectPath,
127
+ agentName: "TestAgent",
128
+ program: "opencode",
129
+ model: "claude-sonnet",
130
+ taskDescription: "Testing debug tools",
131
+ });
132
+
133
+ const result = await debugAgent({ projectPath, agentName: "TestAgent" });
134
+
135
+ expect(result.agent).not.toBeNull();
136
+ expect(result.agent!.name).toBe("TestAgent");
137
+ expect(result.agent!.program).toBe("opencode");
138
+ expect(result.agent!.model).toBe("claude-sonnet");
139
+ expect(result.agent!.task_description).toBe("Testing debug tools");
140
+ });
141
+
142
+ it("includes message counts", async () => {
143
+ await initAgent({ projectPath, agentName: "Agent1" });
144
+ await initAgent({ projectPath, agentName: "Agent2" });
145
+
146
+ await sendAgentMessage({
147
+ projectPath,
148
+ fromAgent: "Agent1",
149
+ toAgents: ["Agent2"],
150
+ subject: "Test 1",
151
+ body: "Hello",
152
+ });
153
+ await sendAgentMessage({
154
+ projectPath,
155
+ fromAgent: "Agent1",
156
+ toAgents: ["Agent2"],
157
+ subject: "Test 2",
158
+ body: "World",
159
+ });
160
+
161
+ const result = await debugAgent({ projectPath, agentName: "Agent1" });
162
+
163
+ expect(result.stats.messagesSent).toBe(2);
164
+ expect(result.stats.messagesReceived).toBe(0);
165
+ });
166
+
167
+ it("includes active reservations", async () => {
168
+ await initAgent({ projectPath, agentName: "Agent1" });
169
+ await reserveAgentFiles({
170
+ projectPath,
171
+ agentName: "Agent1",
172
+ paths: ["src/a.ts", "src/b.ts"],
173
+ reason: "Testing",
174
+ });
175
+
176
+ const result = await debugAgent({ projectPath, agentName: "Agent1" });
177
+
178
+ expect(result.reservations).toHaveLength(2);
179
+ expect(result.reservations.map((r) => r.path)).toContain("src/a.ts");
180
+ expect(result.reservations.map((r) => r.path)).toContain("src/b.ts");
181
+ });
182
+
183
+ it("includes recent events for the agent", async () => {
184
+ await initAgent({ projectPath, agentName: "Agent1" });
185
+ await sendAgentMessage({
186
+ projectPath,
187
+ fromAgent: "Agent1",
188
+ toAgents: ["Agent2"],
189
+ subject: "Test",
190
+ body: "Hello",
191
+ });
192
+
193
+ const result = await debugAgent({
194
+ projectPath,
195
+ agentName: "Agent1",
196
+ includeEvents: true,
197
+ });
198
+
199
+ expect(result.recentEvents).toBeDefined();
200
+ expect(result.recentEvents!.length).toBeGreaterThan(0);
201
+ });
202
+
203
+ it("returns null for non-existent agent", async () => {
204
+ const result = await debugAgent({
205
+ projectPath,
206
+ agentName: "NonExistent",
207
+ });
208
+
209
+ expect(result.agent).toBeNull();
210
+ });
211
+ });
212
+
213
+ // ============================================================================
214
+ // debugMessage - Full message audit trail
215
+ // ============================================================================
216
+
217
+ describe("debugMessage", () => {
218
+ it("returns message with full audit trail", async () => {
219
+ await initAgent({ projectPath, agentName: "Agent1" });
220
+ await initAgent({ projectPath, agentName: "Agent2" });
221
+
222
+ const sendResult = await sendAgentMessage({
223
+ projectPath,
224
+ fromAgent: "Agent1",
225
+ toAgents: ["Agent2"],
226
+ subject: "Important",
227
+ body: "Please review",
228
+ importance: "high",
229
+ threadId: "thread-123",
230
+ });
231
+
232
+ const result = await debugMessage({
233
+ projectPath,
234
+ messageId: sendResult.messageId,
235
+ });
236
+
237
+ expect(result.message).not.toBeNull();
238
+ expect(result.message!.from_agent).toBe("Agent1");
239
+ expect(result.message!.subject).toBe("Important");
240
+ expect(result.message!.body).toBe("Please review");
241
+ expect(result.message!.importance).toBe("high");
242
+ expect(result.message!.thread_id).toBe("thread-123");
243
+ });
244
+
245
+ it("includes recipient status", async () => {
246
+ await initAgent({ projectPath, agentName: "Agent1" });
247
+ await initAgent({ projectPath, agentName: "Agent2" });
248
+ await initAgent({ projectPath, agentName: "Agent3" });
249
+
250
+ const sendResult = await sendAgentMessage({
251
+ projectPath,
252
+ fromAgent: "Agent1",
253
+ toAgents: ["Agent2", "Agent3"],
254
+ subject: "Test",
255
+ body: "Hello",
256
+ });
257
+
258
+ const result = await debugMessage({
259
+ projectPath,
260
+ messageId: sendResult.messageId,
261
+ });
262
+
263
+ expect(result.recipients).toHaveLength(2);
264
+ expect(result.recipients.map((r) => r.agent_name)).toContain("Agent2");
265
+ expect(result.recipients.map((r) => r.agent_name)).toContain("Agent3");
266
+ // Initially unread
267
+ expect(result.recipients.every((r) => r.read_at === null)).toBe(true);
268
+ });
269
+
270
+ it("includes related events", async () => {
271
+ await initAgent({ projectPath, agentName: "Agent1" });
272
+
273
+ const sendResult = await sendAgentMessage({
274
+ projectPath,
275
+ fromAgent: "Agent1",
276
+ toAgents: ["Agent2"],
277
+ subject: "Test",
278
+ body: "Hello",
279
+ });
280
+
281
+ const result = await debugMessage({
282
+ projectPath,
283
+ messageId: sendResult.messageId,
284
+ includeEvents: true,
285
+ });
286
+
287
+ expect(result.events).toBeDefined();
288
+ expect(result.events!.some((e) => e.type === "message_sent")).toBe(true);
289
+ });
290
+
291
+ it("returns null for non-existent message", async () => {
292
+ const result = await debugMessage({
293
+ projectPath,
294
+ messageId: 99999,
295
+ });
296
+
297
+ expect(result.message).toBeNull();
298
+ });
299
+ });
300
+
301
+ // ============================================================================
302
+ // debugReservations - Current reservation state
303
+ // ============================================================================
304
+
305
+ describe("debugReservations", () => {
306
+ it("returns all active reservations", async () => {
307
+ await initAgent({ projectPath, agentName: "Agent1" });
308
+ await initAgent({ projectPath, agentName: "Agent2" });
309
+
310
+ await reserveAgentFiles({
311
+ projectPath,
312
+ agentName: "Agent1",
313
+ paths: ["src/a.ts"],
314
+ reason: "Working on A",
315
+ });
316
+ await reserveAgentFiles({
317
+ projectPath,
318
+ agentName: "Agent2",
319
+ paths: ["src/b.ts"],
320
+ reason: "Working on B",
321
+ });
322
+
323
+ const result = await debugReservations({ projectPath });
324
+
325
+ expect(result.reservations).toHaveLength(2);
326
+ expect(result.byAgent).toHaveProperty("Agent1");
327
+ expect(result.byAgent).toHaveProperty("Agent2");
328
+ });
329
+
330
+ it("groups reservations by agent", async () => {
331
+ await initAgent({ projectPath, agentName: "Agent1" });
332
+
333
+ await reserveAgentFiles({
334
+ projectPath,
335
+ agentName: "Agent1",
336
+ paths: ["src/a.ts", "src/b.ts", "src/c.ts"],
337
+ reason: "Working",
338
+ });
339
+
340
+ const result = await debugReservations({ projectPath });
341
+
342
+ expect(result.byAgent.Agent1).toHaveLength(3);
343
+ });
344
+
345
+ it("includes expiration info", async () => {
346
+ await initAgent({ projectPath, agentName: "Agent1" });
347
+
348
+ await reserveAgentFiles({
349
+ projectPath,
350
+ agentName: "Agent1",
351
+ paths: ["src/a.ts"],
352
+ ttlSeconds: 3600,
353
+ });
354
+
355
+ const result = await debugReservations({ projectPath });
356
+
357
+ expect(result.reservations[0]).toHaveProperty("expires_at");
358
+ expect(result.reservations[0]).toHaveProperty("expires_in_human");
359
+ expect(typeof result.reservations[0].expires_in_human).toBe("string");
360
+ });
361
+
362
+ it("detects potential conflicts", async () => {
363
+ await initAgent({ projectPath, agentName: "Agent1" });
364
+ await initAgent({ projectPath, agentName: "Agent2" });
365
+
366
+ // Agent1 reserves src/**
367
+ await reserveAgentFiles({
368
+ projectPath,
369
+ agentName: "Agent1",
370
+ paths: ["src/**"],
371
+ reason: "Broad reservation",
372
+ });
373
+
374
+ // Agent2 forces reservation of src/specific.ts (to test conflict detection)
375
+ await reserveAgentFiles({
376
+ projectPath,
377
+ agentName: "Agent2",
378
+ paths: ["src/specific.ts"],
379
+ reason: "Specific file",
380
+ force: true, // Force to create overlapping reservation for conflict test
381
+ });
382
+
383
+ const result = await debugReservations({
384
+ projectPath,
385
+ checkConflicts: true,
386
+ });
387
+
388
+ expect(result.conflicts).toBeDefined();
389
+ expect(result.conflicts!.length).toBeGreaterThan(0);
390
+ });
391
+ });
392
+
393
+ // ============================================================================
394
+ // getEventTimeline - Visual timeline of events
395
+ // ============================================================================
396
+
397
+ describe("getEventTimeline", () => {
398
+ it("returns events formatted for timeline display", async () => {
399
+ await initAgent({ projectPath, agentName: "Agent1" });
400
+ await sendAgentMessage({
401
+ projectPath,
402
+ fromAgent: "Agent1",
403
+ toAgents: ["Agent2"],
404
+ subject: "Test",
405
+ body: "Hello",
406
+ });
407
+
408
+ const result = await getEventTimeline({ projectPath });
409
+
410
+ expect(result.timeline).toBeDefined();
411
+ expect(Array.isArray(result.timeline)).toBe(true);
412
+ expect(result.timeline.length).toBeGreaterThan(0);
413
+
414
+ // Each entry should have display-friendly format
415
+ const entry = result.timeline[0];
416
+ expect(entry).toHaveProperty("time");
417
+ expect(entry).toHaveProperty("type");
418
+ expect(entry).toHaveProperty("summary");
419
+ expect(entry).toHaveProperty("agent");
420
+ });
421
+
422
+ it("filters by time range", async () => {
423
+ await initAgent({ projectPath, agentName: "Agent1" });
424
+
425
+ // Wait to ensure timestamp separation
426
+ await new Promise((r) => setTimeout(r, 5));
427
+ const afterFirst = Date.now();
428
+
429
+ // Wait a bit more
430
+ await new Promise((r) => setTimeout(r, 5));
431
+
432
+ await initAgent({ projectPath, agentName: "Agent2" });
433
+
434
+ const result = await getEventTimeline({
435
+ projectPath,
436
+ since: afterFirst,
437
+ });
438
+
439
+ // Should only include Agent2's registration
440
+ expect(result.timeline).toHaveLength(1);
441
+ expect(result.timeline[0].agent).toBe("Agent2");
442
+ });
443
+ });
444
+
445
+ // ============================================================================
446
+ // inspectState - Full state dump for debugging
447
+ // ============================================================================
448
+
449
+ describe("inspectState", () => {
450
+ it("returns complete state snapshot", async () => {
451
+ await initAgent({ projectPath, agentName: "Agent1" });
452
+ await sendAgentMessage({
453
+ projectPath,
454
+ fromAgent: "Agent1",
455
+ toAgents: ["Agent2"],
456
+ subject: "Test",
457
+ body: "Hello",
458
+ });
459
+ await reserveAgentFiles({
460
+ projectPath,
461
+ agentName: "Agent1",
462
+ paths: ["src/a.ts"],
463
+ });
464
+
465
+ const result = await inspectState({ projectPath });
466
+
467
+ expect(result).toHaveProperty("agents");
468
+ expect(result).toHaveProperty("messages");
469
+ expect(result).toHaveProperty("reservations");
470
+ expect(result).toHaveProperty("eventCount");
471
+ expect(result).toHaveProperty("latestSequence");
472
+
473
+ expect(result.agents).toHaveLength(1);
474
+ expect(result.messages).toHaveLength(1);
475
+ expect(result.reservations).toHaveLength(1);
476
+ });
477
+
478
+ it("includes database stats", async () => {
479
+ await initAgent({ projectPath, agentName: "Agent1" });
480
+
481
+ const result = await inspectState({ projectPath });
482
+
483
+ expect(result.stats).toBeDefined();
484
+ expect(result.stats).toHaveProperty("events");
485
+ expect(result.stats).toHaveProperty("agents");
486
+ expect(result.stats).toHaveProperty("messages");
487
+ expect(result.stats).toHaveProperty("reservations");
488
+ });
489
+
490
+ it("can export as JSON string", async () => {
491
+ await initAgent({ projectPath, agentName: "Agent1" });
492
+
493
+ const result = await inspectState({ projectPath, format: "json" });
494
+
495
+ expect(typeof result.json).toBe("string");
496
+ const parsed = JSON.parse(result.json!);
497
+ expect(parsed).toHaveProperty("agents");
498
+ });
499
+ });
500
+ });