@os-eco/overstory-cli 0.6.1

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/agents/builder.md +137 -0
  4. package/agents/coordinator.md +263 -0
  5. package/agents/lead.md +301 -0
  6. package/agents/merger.md +160 -0
  7. package/agents/monitor.md +214 -0
  8. package/agents/reviewer.md +140 -0
  9. package/agents/scout.md +119 -0
  10. package/agents/supervisor.md +423 -0
  11. package/package.json +47 -0
  12. package/src/agents/checkpoint.test.ts +88 -0
  13. package/src/agents/checkpoint.ts +101 -0
  14. package/src/agents/hooks-deployer.test.ts +2040 -0
  15. package/src/agents/hooks-deployer.ts +607 -0
  16. package/src/agents/identity.test.ts +603 -0
  17. package/src/agents/identity.ts +384 -0
  18. package/src/agents/lifecycle.test.ts +196 -0
  19. package/src/agents/lifecycle.ts +183 -0
  20. package/src/agents/manifest.test.ts +746 -0
  21. package/src/agents/manifest.ts +354 -0
  22. package/src/agents/overlay.test.ts +676 -0
  23. package/src/agents/overlay.ts +308 -0
  24. package/src/beads/client.test.ts +217 -0
  25. package/src/beads/client.ts +202 -0
  26. package/src/beads/molecules.test.ts +338 -0
  27. package/src/beads/molecules.ts +198 -0
  28. package/src/commands/agents.test.ts +322 -0
  29. package/src/commands/agents.ts +287 -0
  30. package/src/commands/clean.test.ts +670 -0
  31. package/src/commands/clean.ts +618 -0
  32. package/src/commands/completions.test.ts +342 -0
  33. package/src/commands/completions.ts +887 -0
  34. package/src/commands/coordinator.test.ts +1530 -0
  35. package/src/commands/coordinator.ts +733 -0
  36. package/src/commands/costs.test.ts +1119 -0
  37. package/src/commands/costs.ts +564 -0
  38. package/src/commands/dashboard.test.ts +308 -0
  39. package/src/commands/dashboard.ts +838 -0
  40. package/src/commands/doctor.test.ts +294 -0
  41. package/src/commands/doctor.ts +213 -0
  42. package/src/commands/errors.test.ts +647 -0
  43. package/src/commands/errors.ts +248 -0
  44. package/src/commands/feed.test.ts +578 -0
  45. package/src/commands/feed.ts +361 -0
  46. package/src/commands/group.test.ts +262 -0
  47. package/src/commands/group.ts +511 -0
  48. package/src/commands/hooks.test.ts +458 -0
  49. package/src/commands/hooks.ts +253 -0
  50. package/src/commands/init.test.ts +347 -0
  51. package/src/commands/init.ts +650 -0
  52. package/src/commands/inspect.test.ts +670 -0
  53. package/src/commands/inspect.ts +431 -0
  54. package/src/commands/log.test.ts +1454 -0
  55. package/src/commands/log.ts +724 -0
  56. package/src/commands/logs.test.ts +379 -0
  57. package/src/commands/logs.ts +546 -0
  58. package/src/commands/mail.test.ts +1270 -0
  59. package/src/commands/mail.ts +771 -0
  60. package/src/commands/merge.test.ts +670 -0
  61. package/src/commands/merge.ts +355 -0
  62. package/src/commands/metrics.test.ts +444 -0
  63. package/src/commands/metrics.ts +143 -0
  64. package/src/commands/monitor.test.ts +191 -0
  65. package/src/commands/monitor.ts +390 -0
  66. package/src/commands/nudge.test.ts +230 -0
  67. package/src/commands/nudge.ts +372 -0
  68. package/src/commands/prime.test.ts +470 -0
  69. package/src/commands/prime.ts +381 -0
  70. package/src/commands/replay.test.ts +741 -0
  71. package/src/commands/replay.ts +360 -0
  72. package/src/commands/run.test.ts +431 -0
  73. package/src/commands/run.ts +351 -0
  74. package/src/commands/sling.test.ts +657 -0
  75. package/src/commands/sling.ts +661 -0
  76. package/src/commands/spec.test.ts +203 -0
  77. package/src/commands/spec.ts +168 -0
  78. package/src/commands/status.test.ts +430 -0
  79. package/src/commands/status.ts +398 -0
  80. package/src/commands/stop.test.ts +420 -0
  81. package/src/commands/stop.ts +151 -0
  82. package/src/commands/supervisor.test.ts +187 -0
  83. package/src/commands/supervisor.ts +535 -0
  84. package/src/commands/trace.test.ts +745 -0
  85. package/src/commands/trace.ts +325 -0
  86. package/src/commands/watch.test.ts +145 -0
  87. package/src/commands/watch.ts +247 -0
  88. package/src/commands/worktree.test.ts +786 -0
  89. package/src/commands/worktree.ts +311 -0
  90. package/src/config.test.ts +822 -0
  91. package/src/config.ts +829 -0
  92. package/src/doctor/agents.test.ts +454 -0
  93. package/src/doctor/agents.ts +396 -0
  94. package/src/doctor/config-check.test.ts +190 -0
  95. package/src/doctor/config-check.ts +183 -0
  96. package/src/doctor/consistency.test.ts +651 -0
  97. package/src/doctor/consistency.ts +294 -0
  98. package/src/doctor/databases.test.ts +290 -0
  99. package/src/doctor/databases.ts +218 -0
  100. package/src/doctor/dependencies.test.ts +184 -0
  101. package/src/doctor/dependencies.ts +175 -0
  102. package/src/doctor/logs.test.ts +251 -0
  103. package/src/doctor/logs.ts +295 -0
  104. package/src/doctor/merge-queue.test.ts +216 -0
  105. package/src/doctor/merge-queue.ts +144 -0
  106. package/src/doctor/structure.test.ts +291 -0
  107. package/src/doctor/structure.ts +198 -0
  108. package/src/doctor/types.ts +37 -0
  109. package/src/doctor/version.test.ts +136 -0
  110. package/src/doctor/version.ts +129 -0
  111. package/src/e2e/init-sling-lifecycle.test.ts +277 -0
  112. package/src/errors.ts +217 -0
  113. package/src/events/store.test.ts +660 -0
  114. package/src/events/store.ts +369 -0
  115. package/src/events/tool-filter.test.ts +330 -0
  116. package/src/events/tool-filter.ts +126 -0
  117. package/src/index.ts +316 -0
  118. package/src/insights/analyzer.test.ts +466 -0
  119. package/src/insights/analyzer.ts +203 -0
  120. package/src/logging/color.test.ts +142 -0
  121. package/src/logging/color.ts +71 -0
  122. package/src/logging/logger.test.ts +813 -0
  123. package/src/logging/logger.ts +266 -0
  124. package/src/logging/reporter.test.ts +259 -0
  125. package/src/logging/reporter.ts +109 -0
  126. package/src/logging/sanitizer.test.ts +190 -0
  127. package/src/logging/sanitizer.ts +57 -0
  128. package/src/mail/broadcast.test.ts +203 -0
  129. package/src/mail/broadcast.ts +92 -0
  130. package/src/mail/client.test.ts +773 -0
  131. package/src/mail/client.ts +223 -0
  132. package/src/mail/store.test.ts +705 -0
  133. package/src/mail/store.ts +387 -0
  134. package/src/merge/queue.test.ts +359 -0
  135. package/src/merge/queue.ts +231 -0
  136. package/src/merge/resolver.test.ts +1345 -0
  137. package/src/merge/resolver.ts +645 -0
  138. package/src/metrics/store.test.ts +667 -0
  139. package/src/metrics/store.ts +445 -0
  140. package/src/metrics/summary.test.ts +398 -0
  141. package/src/metrics/summary.ts +178 -0
  142. package/src/metrics/transcript.test.ts +356 -0
  143. package/src/metrics/transcript.ts +175 -0
  144. package/src/mulch/client.test.ts +671 -0
  145. package/src/mulch/client.ts +332 -0
  146. package/src/sessions/compat.test.ts +280 -0
  147. package/src/sessions/compat.ts +104 -0
  148. package/src/sessions/store.test.ts +873 -0
  149. package/src/sessions/store.ts +494 -0
  150. package/src/test-helpers.test.ts +124 -0
  151. package/src/test-helpers.ts +126 -0
  152. package/src/tracker/beads.ts +56 -0
  153. package/src/tracker/factory.test.ts +80 -0
  154. package/src/tracker/factory.ts +64 -0
  155. package/src/tracker/seeds.ts +182 -0
  156. package/src/tracker/types.ts +52 -0
  157. package/src/types.ts +724 -0
  158. package/src/watchdog/daemon.test.ts +1975 -0
  159. package/src/watchdog/daemon.ts +671 -0
  160. package/src/watchdog/health.test.ts +431 -0
  161. package/src/watchdog/health.ts +264 -0
  162. package/src/watchdog/triage.test.ts +164 -0
  163. package/src/watchdog/triage.ts +179 -0
  164. package/src/worktree/manager.test.ts +439 -0
  165. package/src/worktree/manager.ts +198 -0
  166. package/src/worktree/tmux.test.ts +1009 -0
  167. package/src/worktree/tmux.ts +509 -0
  168. package/templates/CLAUDE.md.tmpl +89 -0
  169. package/templates/hooks.json.tmpl +105 -0
  170. package/templates/overlay.md.tmpl +81 -0
@@ -0,0 +1,705 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { MailError } from "../errors.ts";
6
+ import type { MailMessage } from "../types.ts";
7
+ import { createMailStore, type MailStore } from "./store.ts";
8
+
9
+ describe("createMailStore", () => {
10
+ let tempDir: string;
11
+ let store: MailStore;
12
+
13
+ beforeEach(async () => {
14
+ tempDir = await mkdtemp(join(tmpdir(), "overstory-mail-test-"));
15
+ store = createMailStore(join(tempDir, "mail.db"));
16
+ });
17
+
18
+ afterEach(async () => {
19
+ store.close();
20
+ await rm(tempDir, { recursive: true, force: true });
21
+ });
22
+
23
+ describe("insert", () => {
24
+ test("inserts a message and returns it with generated id and timestamp", () => {
25
+ const msg = store.insert({
26
+ id: "",
27
+ from: "agent-a",
28
+ to: "orchestrator",
29
+ subject: "status update",
30
+ body: "All tests passing",
31
+ type: "status",
32
+ priority: "normal",
33
+ threadId: null,
34
+ });
35
+
36
+ expect(msg.id).toMatch(/^msg-[a-z0-9]{12}$/);
37
+ expect(msg.from).toBe("agent-a");
38
+ expect(msg.to).toBe("orchestrator");
39
+ expect(msg.subject).toBe("status update");
40
+ expect(msg.body).toBe("All tests passing");
41
+ expect(msg.type).toBe("status");
42
+ expect(msg.priority).toBe("normal");
43
+ expect(msg.threadId).toBeNull();
44
+ expect(msg.read).toBe(false);
45
+ expect(msg.createdAt).toBeTruthy();
46
+ });
47
+
48
+ test("uses provided id if non-empty", () => {
49
+ const msg = store.insert({
50
+ id: "custom-id-123",
51
+ from: "agent-a",
52
+ to: "orchestrator",
53
+ subject: "test",
54
+ body: "test body",
55
+ type: "status",
56
+ priority: "normal",
57
+ threadId: null,
58
+ });
59
+
60
+ expect(msg.id).toBe("custom-id-123");
61
+ });
62
+
63
+ test("throws MailError on duplicate id", () => {
64
+ store.insert({
65
+ id: "dupe-id",
66
+ from: "agent-a",
67
+ to: "orchestrator",
68
+ subject: "first",
69
+ body: "first message",
70
+ type: "status",
71
+ priority: "normal",
72
+ threadId: null,
73
+ });
74
+
75
+ expect(() =>
76
+ store.insert({
77
+ id: "dupe-id",
78
+ from: "agent-b",
79
+ to: "orchestrator",
80
+ subject: "second",
81
+ body: "second message",
82
+ type: "status",
83
+ priority: "normal",
84
+ threadId: null,
85
+ }),
86
+ ).toThrow(MailError);
87
+ });
88
+ });
89
+
90
+ describe("getById", () => {
91
+ test("returns message by id", () => {
92
+ store.insert({
93
+ id: "msg-test-001",
94
+ from: "agent-a",
95
+ to: "orchestrator",
96
+ subject: "test",
97
+ body: "body",
98
+ type: "status",
99
+ priority: "normal",
100
+ threadId: null,
101
+ });
102
+
103
+ const msg = store.getById("msg-test-001");
104
+ expect(msg).not.toBeNull();
105
+ expect(msg?.id).toBe("msg-test-001");
106
+ expect(msg?.from).toBe("agent-a");
107
+ });
108
+
109
+ test("returns null for non-existent id", () => {
110
+ const msg = store.getById("nonexistent");
111
+ expect(msg).toBeNull();
112
+ });
113
+ });
114
+
115
+ describe("getUnread", () => {
116
+ test("returns unread messages for a specific agent", () => {
117
+ store.insert({
118
+ id: "",
119
+ from: "agent-a",
120
+ to: "orchestrator",
121
+ subject: "msg1",
122
+ body: "body1",
123
+ type: "status",
124
+ priority: "normal",
125
+ threadId: null,
126
+ });
127
+ store.insert({
128
+ id: "",
129
+ from: "agent-b",
130
+ to: "orchestrator",
131
+ subject: "msg2",
132
+ body: "body2",
133
+ type: "status",
134
+ priority: "normal",
135
+ threadId: null,
136
+ });
137
+ store.insert({
138
+ id: "",
139
+ from: "agent-a",
140
+ to: "agent-c",
141
+ subject: "msg3",
142
+ body: "body3",
143
+ type: "status",
144
+ priority: "normal",
145
+ threadId: null,
146
+ });
147
+
148
+ const unread = store.getUnread("orchestrator");
149
+ expect(unread).toHaveLength(2);
150
+ expect(unread[0]?.subject).toBe("msg1");
151
+ expect(unread[1]?.subject).toBe("msg2");
152
+ });
153
+
154
+ test("returns empty array when no unread messages", () => {
155
+ const unread = store.getUnread("orchestrator");
156
+ expect(unread).toHaveLength(0);
157
+ });
158
+
159
+ test("does not return already-read messages", () => {
160
+ const msg = store.insert({
161
+ id: "",
162
+ from: "agent-a",
163
+ to: "orchestrator",
164
+ subject: "test",
165
+ body: "body",
166
+ type: "status",
167
+ priority: "normal",
168
+ threadId: null,
169
+ });
170
+ store.markRead(msg.id);
171
+
172
+ const unread = store.getUnread("orchestrator");
173
+ expect(unread).toHaveLength(0);
174
+ });
175
+
176
+ test("returns messages in chronological order (ASC)", () => {
177
+ store.insert({
178
+ id: "msg-first",
179
+ from: "agent-a",
180
+ to: "orchestrator",
181
+ subject: "first",
182
+ body: "body",
183
+ type: "status",
184
+ priority: "normal",
185
+ threadId: null,
186
+ });
187
+ store.insert({
188
+ id: "msg-second",
189
+ from: "agent-b",
190
+ to: "orchestrator",
191
+ subject: "second",
192
+ body: "body",
193
+ type: "status",
194
+ priority: "normal",
195
+ threadId: null,
196
+ });
197
+
198
+ const unread = store.getUnread("orchestrator");
199
+ expect(unread[0]?.id).toBe("msg-first");
200
+ expect(unread[1]?.id).toBe("msg-second");
201
+ });
202
+ });
203
+
204
+ describe("markRead", () => {
205
+ test("marks a message as read", () => {
206
+ const msg = store.insert({
207
+ id: "msg-to-read",
208
+ from: "agent-a",
209
+ to: "orchestrator",
210
+ subject: "test",
211
+ body: "body",
212
+ type: "status",
213
+ priority: "normal",
214
+ threadId: null,
215
+ });
216
+
217
+ store.markRead(msg.id);
218
+
219
+ const fetched = store.getById(msg.id);
220
+ expect(fetched?.read).toBe(true);
221
+ });
222
+
223
+ test("is idempotent (marking already-read message does not error)", () => {
224
+ const msg = store.insert({
225
+ id: "msg-idempotent",
226
+ from: "agent-a",
227
+ to: "orchestrator",
228
+ subject: "test",
229
+ body: "body",
230
+ type: "status",
231
+ priority: "normal",
232
+ threadId: null,
233
+ });
234
+
235
+ store.markRead(msg.id);
236
+ store.markRead(msg.id);
237
+
238
+ const fetched = store.getById(msg.id);
239
+ expect(fetched?.read).toBe(true);
240
+ });
241
+ });
242
+
243
+ describe("getAll", () => {
244
+ test("returns all messages without filters", () => {
245
+ store.insert({
246
+ id: "",
247
+ from: "agent-a",
248
+ to: "orchestrator",
249
+ subject: "msg1",
250
+ body: "body1",
251
+ type: "status",
252
+ priority: "normal",
253
+ threadId: null,
254
+ });
255
+ store.insert({
256
+ id: "",
257
+ from: "agent-b",
258
+ to: "agent-c",
259
+ subject: "msg2",
260
+ body: "body2",
261
+ type: "question",
262
+ priority: "high",
263
+ threadId: null,
264
+ });
265
+
266
+ const all = store.getAll();
267
+ expect(all).toHaveLength(2);
268
+ });
269
+
270
+ test("filters by from", () => {
271
+ store.insert({
272
+ id: "",
273
+ from: "agent-a",
274
+ to: "orchestrator",
275
+ subject: "msg1",
276
+ body: "body1",
277
+ type: "status",
278
+ priority: "normal",
279
+ threadId: null,
280
+ });
281
+ store.insert({
282
+ id: "",
283
+ from: "agent-b",
284
+ to: "orchestrator",
285
+ subject: "msg2",
286
+ body: "body2",
287
+ type: "status",
288
+ priority: "normal",
289
+ threadId: null,
290
+ });
291
+
292
+ const filtered = store.getAll({ from: "agent-a" });
293
+ expect(filtered).toHaveLength(1);
294
+ expect(filtered[0]?.from).toBe("agent-a");
295
+ });
296
+
297
+ test("filters by to", () => {
298
+ store.insert({
299
+ id: "",
300
+ from: "agent-a",
301
+ to: "orchestrator",
302
+ subject: "msg1",
303
+ body: "body1",
304
+ type: "status",
305
+ priority: "normal",
306
+ threadId: null,
307
+ });
308
+ store.insert({
309
+ id: "",
310
+ from: "agent-a",
311
+ to: "agent-b",
312
+ subject: "msg2",
313
+ body: "body2",
314
+ type: "status",
315
+ priority: "normal",
316
+ threadId: null,
317
+ });
318
+
319
+ const filtered = store.getAll({ to: "agent-b" });
320
+ expect(filtered).toHaveLength(1);
321
+ expect(filtered[0]?.to).toBe("agent-b");
322
+ });
323
+
324
+ test("filters by unread", () => {
325
+ const msg1 = store.insert({
326
+ id: "",
327
+ from: "agent-a",
328
+ to: "orchestrator",
329
+ subject: "msg1",
330
+ body: "body1",
331
+ type: "status",
332
+ priority: "normal",
333
+ threadId: null,
334
+ });
335
+ store.insert({
336
+ id: "",
337
+ from: "agent-b",
338
+ to: "orchestrator",
339
+ subject: "msg2",
340
+ body: "body2",
341
+ type: "status",
342
+ priority: "normal",
343
+ threadId: null,
344
+ });
345
+ store.markRead(msg1.id);
346
+
347
+ const unreadOnly = store.getAll({ unread: true });
348
+ expect(unreadOnly).toHaveLength(1);
349
+ expect(unreadOnly[0]?.subject).toBe("msg2");
350
+
351
+ const readOnly = store.getAll({ unread: false });
352
+ expect(readOnly).toHaveLength(1);
353
+ expect(readOnly[0]?.subject).toBe("msg1");
354
+ });
355
+
356
+ test("respects limit option", () => {
357
+ for (let i = 1; i <= 5; i++) {
358
+ store.insert({
359
+ id: "",
360
+ from: "agent-a",
361
+ to: "orchestrator",
362
+ subject: `msg${i}`,
363
+ body: `body${i}`,
364
+ type: "status",
365
+ priority: "normal",
366
+ threadId: null,
367
+ });
368
+ }
369
+
370
+ const limited = store.getAll({ limit: 3 });
371
+ expect(limited).toHaveLength(3);
372
+ });
373
+
374
+ test("limit combined with filter", () => {
375
+ for (let i = 1; i <= 4; i++) {
376
+ store.insert({
377
+ id: "",
378
+ from: "agent-a",
379
+ to: "orchestrator",
380
+ subject: `a-msg${i}`,
381
+ body: `body`,
382
+ type: "status",
383
+ priority: "normal",
384
+ threadId: null,
385
+ });
386
+ }
387
+ store.insert({
388
+ id: "",
389
+ from: "agent-b",
390
+ to: "orchestrator",
391
+ subject: "b-msg",
392
+ body: "body",
393
+ type: "status",
394
+ priority: "normal",
395
+ threadId: null,
396
+ });
397
+
398
+ const limited = store.getAll({ from: "agent-a", limit: 2 });
399
+ expect(limited).toHaveLength(2);
400
+ expect(limited.every((m) => m.from === "agent-a")).toBe(true);
401
+ });
402
+
403
+ test("combines multiple filters", () => {
404
+ store.insert({
405
+ id: "",
406
+ from: "agent-a",
407
+ to: "orchestrator",
408
+ subject: "msg1",
409
+ body: "body1",
410
+ type: "status",
411
+ priority: "normal",
412
+ threadId: null,
413
+ });
414
+ store.insert({
415
+ id: "",
416
+ from: "agent-a",
417
+ to: "agent-b",
418
+ subject: "msg2",
419
+ body: "body2",
420
+ type: "status",
421
+ priority: "normal",
422
+ threadId: null,
423
+ });
424
+ store.insert({
425
+ id: "",
426
+ from: "agent-b",
427
+ to: "orchestrator",
428
+ subject: "msg3",
429
+ body: "body3",
430
+ type: "status",
431
+ priority: "normal",
432
+ threadId: null,
433
+ });
434
+
435
+ const filtered = store.getAll({ from: "agent-a", to: "orchestrator" });
436
+ expect(filtered).toHaveLength(1);
437
+ expect(filtered[0]?.subject).toBe("msg1");
438
+ });
439
+ });
440
+
441
+ describe("getByThread", () => {
442
+ test("returns messages in the same thread", () => {
443
+ store.insert({
444
+ id: "msg-thread-1",
445
+ from: "agent-a",
446
+ to: "orchestrator",
447
+ subject: "question",
448
+ body: "first message",
449
+ type: "question",
450
+ priority: "normal",
451
+ threadId: "thread-123",
452
+ });
453
+ store.insert({
454
+ id: "msg-thread-2",
455
+ from: "orchestrator",
456
+ to: "agent-a",
457
+ subject: "Re: question",
458
+ body: "reply",
459
+ type: "status",
460
+ priority: "normal",
461
+ threadId: "thread-123",
462
+ });
463
+ store.insert({
464
+ id: "msg-other",
465
+ from: "agent-b",
466
+ to: "orchestrator",
467
+ subject: "unrelated",
468
+ body: "different thread",
469
+ type: "status",
470
+ priority: "normal",
471
+ threadId: "thread-456",
472
+ });
473
+
474
+ const thread = store.getByThread("thread-123");
475
+ expect(thread).toHaveLength(2);
476
+ expect(thread[0]?.id).toBe("msg-thread-1");
477
+ expect(thread[1]?.id).toBe("msg-thread-2");
478
+ });
479
+
480
+ test("returns empty array for non-existent thread", () => {
481
+ const thread = store.getByThread("nonexistent");
482
+ expect(thread).toHaveLength(0);
483
+ });
484
+ });
485
+
486
+ describe("WAL mode and concurrent access", () => {
487
+ test("second store instance can read while first is writing", () => {
488
+ const store2 = createMailStore(join(tempDir, "mail.db"));
489
+
490
+ store.insert({
491
+ id: "msg-concurrent",
492
+ from: "agent-a",
493
+ to: "orchestrator",
494
+ subject: "test",
495
+ body: "concurrent",
496
+ type: "status",
497
+ priority: "normal",
498
+ threadId: null,
499
+ });
500
+
501
+ const msg = store2.getById("msg-concurrent");
502
+ expect(msg).not.toBeNull();
503
+ expect(msg?.body).toBe("concurrent");
504
+
505
+ store2.close();
506
+ });
507
+ });
508
+
509
+ describe("CHECK constraints", () => {
510
+ test("rejects invalid type at DB level", () => {
511
+ expect(() =>
512
+ store.insert({
513
+ id: "msg-bad-type",
514
+ from: "agent-a",
515
+ to: "orchestrator",
516
+ subject: "test",
517
+ body: "body",
518
+ type: "invalid_type" as MailMessage["type"],
519
+ priority: "normal",
520
+ threadId: null,
521
+ }),
522
+ ).toThrow();
523
+ });
524
+
525
+ test("rejects invalid priority at DB level", () => {
526
+ expect(() =>
527
+ store.insert({
528
+ id: "msg-bad-prio",
529
+ from: "agent-a",
530
+ to: "orchestrator",
531
+ subject: "test",
532
+ body: "body",
533
+ type: "status",
534
+ priority: "invalid_prio" as MailMessage["priority"],
535
+ threadId: null,
536
+ }),
537
+ ).toThrow();
538
+ });
539
+
540
+ test("accepts all valid type values including protocol types", () => {
541
+ const types: MailMessage["type"][] = [
542
+ "status",
543
+ "question",
544
+ "result",
545
+ "error",
546
+ "worker_done",
547
+ "merge_ready",
548
+ "merged",
549
+ "merge_failed",
550
+ "escalation",
551
+ "health_check",
552
+ "dispatch",
553
+ "assign",
554
+ ];
555
+ for (const type of types) {
556
+ const msg = store.insert({
557
+ id: "",
558
+ from: "agent-a",
559
+ to: "orchestrator",
560
+ subject: `type-${type}`,
561
+ body: "body",
562
+ type,
563
+ priority: "normal",
564
+ threadId: null,
565
+ });
566
+ expect(msg.type).toBe(type);
567
+ }
568
+ });
569
+
570
+ test("accepts all valid priority values", () => {
571
+ const priorities: MailMessage["priority"][] = ["low", "normal", "high", "urgent"];
572
+ for (const priority of priorities) {
573
+ const msg = store.insert({
574
+ id: "",
575
+ from: "agent-a",
576
+ to: "orchestrator",
577
+ subject: `prio-${priority}`,
578
+ body: "body",
579
+ type: "status",
580
+ priority,
581
+ threadId: null,
582
+ });
583
+ expect(msg.priority).toBe(priority);
584
+ }
585
+ });
586
+
587
+ test("migrates existing table to add payload column and protocol types", () => {
588
+ // Create a second store to verify migration works on an existing DB
589
+ // The beforeEach already created the DB with constraints,
590
+ // so this tests that reopening is safe
591
+ const store2 = createMailStore(join(tempDir, "mail.db"));
592
+ const msg = store2.insert({
593
+ id: "msg-after-migration",
594
+ from: "agent-a",
595
+ to: "orchestrator",
596
+ subject: "migration test",
597
+ body: "body",
598
+ type: "status",
599
+ priority: "normal",
600
+ threadId: null,
601
+ });
602
+ expect(msg.id).toBe("msg-after-migration");
603
+
604
+ // Invalid values should still be rejected
605
+ expect(() =>
606
+ store2.insert({
607
+ id: "msg-bad-after",
608
+ from: "agent-a",
609
+ to: "orchestrator",
610
+ subject: "test",
611
+ body: "body",
612
+ type: "bogus" as MailMessage["type"],
613
+ priority: "normal",
614
+ threadId: null,
615
+ }),
616
+ ).toThrow();
617
+
618
+ store2.close();
619
+ });
620
+ });
621
+
622
+ describe("payload column", () => {
623
+ test("stores null payload by default when not provided", () => {
624
+ const msg = store.insert({
625
+ id: "msg-no-payload",
626
+ from: "agent-a",
627
+ to: "orchestrator",
628
+ subject: "test",
629
+ body: "body",
630
+ type: "status",
631
+ priority: "normal",
632
+ threadId: null,
633
+ });
634
+
635
+ const fetched = store.getById(msg.id);
636
+ expect(fetched?.payload).toBeNull();
637
+ });
638
+
639
+ test("stores JSON payload string", () => {
640
+ const payload = JSON.stringify({
641
+ beadId: "beads-abc",
642
+ branch: "agent/builder-1",
643
+ exitCode: 0,
644
+ filesModified: ["src/foo.ts"],
645
+ });
646
+ const msg = store.insert({
647
+ id: "msg-with-payload",
648
+ from: "builder-1",
649
+ to: "lead-1",
650
+ subject: "Task complete",
651
+ body: "Implementation finished",
652
+ type: "worker_done",
653
+ priority: "normal",
654
+ threadId: null,
655
+ payload,
656
+ });
657
+
658
+ const fetched = store.getById(msg.id);
659
+ expect(fetched?.payload).toBe(payload);
660
+ expect(fetched?.type).toBe("worker_done");
661
+ });
662
+
663
+ test("returns payload in getUnread results", () => {
664
+ const payload = JSON.stringify({ severity: "critical", beadId: null, context: "OOM" });
665
+ store.insert({
666
+ id: "msg-escalation",
667
+ from: "builder-1",
668
+ to: "orchestrator",
669
+ subject: "Escalation",
670
+ body: "Out of memory",
671
+ type: "escalation",
672
+ priority: "urgent",
673
+ threadId: null,
674
+ payload,
675
+ });
676
+
677
+ const unread = store.getUnread("orchestrator");
678
+ expect(unread).toHaveLength(1);
679
+ expect(unread[0]?.payload).toBe(payload);
680
+ });
681
+
682
+ test("returns payload in getAll results", () => {
683
+ const payload = JSON.stringify({
684
+ branch: "agent/b1",
685
+ beadId: "beads-xyz",
686
+ tier: "clean-merge",
687
+ });
688
+ store.insert({
689
+ id: "msg-merged",
690
+ from: "merger-1",
691
+ to: "lead-1",
692
+ subject: "Merged",
693
+ body: "Branch merged",
694
+ type: "merged",
695
+ priority: "normal",
696
+ threadId: null,
697
+ payload,
698
+ });
699
+
700
+ const all = store.getAll();
701
+ expect(all).toHaveLength(1);
702
+ expect(all[0]?.payload).toBe(payload);
703
+ });
704
+ });
705
+ });