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.
- package/README.md +201 -0
- package/package.json +28 -0
- package/src/adapter.ts +306 -0
- package/src/index.ts +57 -0
- package/src/pglite.ts +189 -0
- package/src/streams/agent-mail.test.ts +777 -0
- package/src/streams/agent-mail.ts +535 -0
- package/src/streams/debug.test.ts +500 -0
- package/src/streams/debug.ts +727 -0
- package/src/streams/effect/ask.integration.test.ts +314 -0
- package/src/streams/effect/ask.ts +202 -0
- package/src/streams/effect/cursor.integration.test.ts +418 -0
- package/src/streams/effect/cursor.ts +288 -0
- package/src/streams/effect/deferred.test.ts +357 -0
- package/src/streams/effect/deferred.ts +445 -0
- package/src/streams/effect/index.ts +17 -0
- package/src/streams/effect/layers.ts +73 -0
- package/src/streams/effect/lock.test.ts +385 -0
- package/src/streams/effect/lock.ts +399 -0
- package/src/streams/effect/mailbox.test.ts +260 -0
- package/src/streams/effect/mailbox.ts +318 -0
- package/src/streams/events.test.ts +924 -0
- package/src/streams/events.ts +329 -0
- package/src/streams/index.test.ts +229 -0
- package/src/streams/index.ts +578 -0
- package/src/streams/migrations.test.ts +359 -0
- package/src/streams/migrations.ts +362 -0
- package/src/streams/projections.test.ts +611 -0
- package/src/streams/projections.ts +564 -0
- package/src/streams/store.integration.test.ts +658 -0
- package/src/streams/store.ts +1129 -0
- package/src/streams/swarm-mail.ts +552 -0
- package/src/types/adapter.ts +392 -0
- package/src/types/database.ts +127 -0
- package/src/types/index.ts +26 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,924 @@
|
|
|
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
|
+
});
|