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,658 @@
1
+ /**
2
+ * Integration tests for Event Store
3
+ *
4
+ * Tests the core event sourcing operations:
5
+ * - Append events
6
+ * - Read events with filters
7
+ * - Materialized view updates
8
+ * - Replay functionality
9
+ */
10
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
11
+ import { rm, mkdir } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+ import {
15
+ appendEvent,
16
+ appendEvents,
17
+ readEvents,
18
+ getLatestSequence,
19
+ replayEvents,
20
+ replayEventsBatched,
21
+ registerAgent,
22
+ sendMessage,
23
+ reserveFiles,
24
+ } from "./store";
25
+ import { createEvent } from "./events";
26
+ import { getDatabase, closeDatabase, getDatabaseStats } from "./index";
27
+
28
+ // Use unique temp directory for each test run
29
+ let TEST_PROJECT_PATH: string;
30
+
31
+ describe("Event Store", () => {
32
+ beforeEach(async () => {
33
+ // Create unique path for each test to ensure isolation
34
+ TEST_PROJECT_PATH = join(
35
+ tmpdir(),
36
+ `streams-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
37
+ );
38
+ // Create the directory so getDatabasePath uses it instead of global
39
+ await mkdir(TEST_PROJECT_PATH, { recursive: true });
40
+ });
41
+
42
+ afterEach(async () => {
43
+ // Close and clean up
44
+ await closeDatabase(TEST_PROJECT_PATH);
45
+ try {
46
+ await rm(join(TEST_PROJECT_PATH, ".opencode"), { recursive: true });
47
+ } catch {
48
+ // Ignore if doesn't exist
49
+ }
50
+ });
51
+
52
+ describe("appendEvent", () => {
53
+ it("should append an event and return with id and sequence", async () => {
54
+ const event = createEvent("agent_registered", {
55
+ project_key: "test-project",
56
+ agent_name: "TestAgent",
57
+ program: "opencode",
58
+ model: "claude-sonnet-4",
59
+ });
60
+
61
+ const result = await appendEvent(event, TEST_PROJECT_PATH);
62
+
63
+ expect(result.id).toBeDefined();
64
+ expect(result.sequence).toBeDefined();
65
+ expect(result.type).toBe("agent_registered");
66
+ // Type narrowing for discriminated union
67
+ if (result.type === "agent_registered") {
68
+ expect(result.agent_name).toBe("TestAgent");
69
+ }
70
+ });
71
+
72
+ it("should update materialized views for agent_registered", async () => {
73
+ const event = createEvent("agent_registered", {
74
+ project_key: "test-project",
75
+ agent_name: "TestAgent",
76
+ program: "opencode",
77
+ model: "claude-sonnet-4",
78
+ task_description: "Testing the event store",
79
+ });
80
+
81
+ await appendEvent(event, TEST_PROJECT_PATH);
82
+
83
+ // Check agents table
84
+ const db = await getDatabase(TEST_PROJECT_PATH);
85
+ const agents = await db.query<{ name: string; task_description: string }>(
86
+ "SELECT name, task_description FROM agents WHERE project_key = $1",
87
+ ["test-project"],
88
+ );
89
+
90
+ expect(agents.rows.length).toBe(1);
91
+ expect(agents.rows[0]?.name).toBe("TestAgent");
92
+ expect(agents.rows[0]?.task_description).toBe("Testing the event store");
93
+ });
94
+ });
95
+
96
+ describe("appendEvents (batch)", () => {
97
+ it("should append multiple events in a transaction", async () => {
98
+ const events = [
99
+ createEvent("agent_registered", {
100
+ project_key: "test-project",
101
+ agent_name: "Agent1",
102
+ program: "opencode",
103
+ model: "claude-sonnet-4",
104
+ }),
105
+ createEvent("agent_registered", {
106
+ project_key: "test-project",
107
+ agent_name: "Agent2",
108
+ program: "opencode",
109
+ model: "claude-haiku",
110
+ }),
111
+ ];
112
+
113
+ const results = await appendEvents(events, TEST_PROJECT_PATH);
114
+
115
+ expect(results.length).toBe(2);
116
+ expect(results[0]?.sequence).toBeLessThan(results[1]?.sequence ?? 0);
117
+ });
118
+ });
119
+
120
+ describe("readEvents", () => {
121
+ it("should read events with filters", async () => {
122
+ // Create some events
123
+ await appendEvent(
124
+ createEvent("agent_registered", {
125
+ project_key: "project-a",
126
+ agent_name: "Agent1",
127
+ program: "opencode",
128
+ model: "claude-sonnet-4",
129
+ }),
130
+ TEST_PROJECT_PATH,
131
+ );
132
+
133
+ await appendEvent(
134
+ createEvent("agent_registered", {
135
+ project_key: "project-b",
136
+ agent_name: "Agent2",
137
+ program: "opencode",
138
+ model: "claude-sonnet-4",
139
+ }),
140
+ TEST_PROJECT_PATH,
141
+ );
142
+
143
+ // Read only project-a events
144
+ const events = await readEvents(
145
+ { projectKey: "project-a" },
146
+ TEST_PROJECT_PATH,
147
+ );
148
+
149
+ expect(events.length).toBe(1);
150
+ expect(events[0]?.project_key).toBe("project-a");
151
+ });
152
+
153
+ it("should filter by event type", async () => {
154
+ await appendEvent(
155
+ createEvent("agent_registered", {
156
+ project_key: "test-project",
157
+ agent_name: "Agent1",
158
+ program: "opencode",
159
+ model: "claude-sonnet-4",
160
+ }),
161
+ TEST_PROJECT_PATH,
162
+ );
163
+
164
+ await appendEvent(
165
+ createEvent("agent_active", {
166
+ project_key: "test-project",
167
+ agent_name: "Agent1",
168
+ }),
169
+ TEST_PROJECT_PATH,
170
+ );
171
+
172
+ const events = await readEvents(
173
+ { types: ["agent_active"] },
174
+ TEST_PROJECT_PATH,
175
+ );
176
+
177
+ expect(events.length).toBe(1);
178
+ expect(events[0]?.type).toBe("agent_active");
179
+ });
180
+
181
+ it("should support pagination", async () => {
182
+ // Create 5 events
183
+ for (let i = 0; i < 5; i++) {
184
+ await appendEvent(
185
+ createEvent("agent_active", {
186
+ project_key: "test-project",
187
+ agent_name: `Agent${i}`,
188
+ }),
189
+ TEST_PROJECT_PATH,
190
+ );
191
+ }
192
+
193
+ const page1 = await readEvents({ limit: 2 }, TEST_PROJECT_PATH);
194
+ const page2 = await readEvents(
195
+ { limit: 2, offset: 2 },
196
+ TEST_PROJECT_PATH,
197
+ );
198
+
199
+ expect(page1.length).toBe(2);
200
+ expect(page2.length).toBe(2);
201
+ expect(page1[0]?.sequence).not.toBe(page2[0]?.sequence);
202
+ });
203
+ });
204
+
205
+ describe("getLatestSequence", () => {
206
+ it("should return 0 for empty database", async () => {
207
+ const seq = await getLatestSequence(undefined, TEST_PROJECT_PATH);
208
+ expect(seq).toBe(0);
209
+ });
210
+
211
+ it("should return latest sequence number", async () => {
212
+ await appendEvent(
213
+ createEvent("agent_registered", {
214
+ project_key: "test-project",
215
+ agent_name: "Agent1",
216
+ program: "opencode",
217
+ model: "claude-sonnet-4",
218
+ }),
219
+ TEST_PROJECT_PATH,
220
+ );
221
+
222
+ await appendEvent(
223
+ createEvent("agent_active", {
224
+ project_key: "test-project",
225
+ agent_name: "Agent1",
226
+ }),
227
+ TEST_PROJECT_PATH,
228
+ );
229
+
230
+ const seq = await getLatestSequence(undefined, TEST_PROJECT_PATH);
231
+ expect(seq).toBe(2);
232
+ });
233
+ });
234
+
235
+ describe("convenience functions", () => {
236
+ it("registerAgent should create agent_registered event", async () => {
237
+ const result = await registerAgent(
238
+ "test-project",
239
+ "MyAgent",
240
+ {
241
+ program: "opencode",
242
+ model: "claude-sonnet-4",
243
+ taskDescription: "Testing",
244
+ },
245
+ TEST_PROJECT_PATH,
246
+ );
247
+
248
+ expect(result.type).toBe("agent_registered");
249
+ expect(result.agent_name).toBe("MyAgent");
250
+ expect(result.task_description).toBe("Testing");
251
+ });
252
+
253
+ it("sendMessage should create message_sent event and update views", async () => {
254
+ // Register agents first
255
+ await registerAgent("test-project", "Sender", {}, TEST_PROJECT_PATH);
256
+ await registerAgent("test-project", "Receiver", {}, TEST_PROJECT_PATH);
257
+
258
+ const result = await sendMessage(
259
+ "test-project",
260
+ "Sender",
261
+ ["Receiver"],
262
+ "Hello",
263
+ "This is a test message",
264
+ { importance: "high" },
265
+ TEST_PROJECT_PATH,
266
+ );
267
+
268
+ expect(result.type).toBe("message_sent");
269
+ expect(result.subject).toBe("Hello");
270
+ expect(result.importance).toBe("high");
271
+
272
+ // Check messages table
273
+ const db = await getDatabase(TEST_PROJECT_PATH);
274
+ const messages = await db.query<{ subject: string; importance: string }>(
275
+ "SELECT subject, importance FROM messages WHERE project_key = $1",
276
+ ["test-project"],
277
+ );
278
+
279
+ expect(messages.rows.length).toBe(1);
280
+ expect(messages.rows[0]?.subject).toBe("Hello");
281
+ });
282
+
283
+ it("reserveFiles should create file_reserved event", async () => {
284
+ await registerAgent("test-project", "Worker", {}, TEST_PROJECT_PATH);
285
+
286
+ const result = await reserveFiles(
287
+ "test-project",
288
+ "Worker",
289
+ ["src/**/*.ts", "tests/**/*.ts"],
290
+ { reason: "Refactoring", exclusive: true, ttlSeconds: 1800 },
291
+ TEST_PROJECT_PATH,
292
+ );
293
+
294
+ expect(result.type).toBe("file_reserved");
295
+ expect(result.paths).toEqual(["src/**/*.ts", "tests/**/*.ts"]);
296
+ expect(result.exclusive).toBe(true);
297
+
298
+ // Check reservations table
299
+ const db = await getDatabase(TEST_PROJECT_PATH);
300
+ const reservations = await db.query<{ path_pattern: string }>(
301
+ "SELECT path_pattern FROM reservations WHERE project_key = $1 AND released_at IS NULL",
302
+ ["test-project"],
303
+ );
304
+
305
+ expect(reservations.rows.length).toBe(2);
306
+ });
307
+ });
308
+
309
+ describe("replayEvents", () => {
310
+ it("should rebuild materialized views from events", async () => {
311
+ // Create some events
312
+ await registerAgent(
313
+ "test-project",
314
+ "Agent1",
315
+ { taskDescription: "Original" },
316
+ TEST_PROJECT_PATH,
317
+ );
318
+
319
+ // Manually corrupt the view
320
+ const db = await getDatabase(TEST_PROJECT_PATH);
321
+ await db.query(
322
+ "UPDATE agents SET task_description = 'Corrupted' WHERE name = 'Agent1'",
323
+ );
324
+
325
+ // Verify corruption
326
+ const corrupted = await db.query<{ task_description: string }>(
327
+ "SELECT task_description FROM agents WHERE name = 'Agent1'",
328
+ );
329
+ expect(corrupted.rows[0]?.task_description).toBe("Corrupted");
330
+
331
+ // Replay events
332
+ const result = await replayEvents(
333
+ { clearViews: true },
334
+ TEST_PROJECT_PATH,
335
+ );
336
+
337
+ expect(result.eventsReplayed).toBe(1);
338
+
339
+ // Verify view is restored
340
+ const restored = await db.query<{ task_description: string }>(
341
+ "SELECT task_description FROM agents WHERE name = 'Agent1'",
342
+ );
343
+ expect(restored.rows[0]?.task_description).toBe("Original");
344
+ });
345
+ });
346
+
347
+ describe("replayEventsBatched", () => {
348
+ it("should replay events in batches with progress tracking", async () => {
349
+ // Create 50 events
350
+ for (let i = 0; i < 50; i++) {
351
+ await registerAgent(
352
+ "test-project",
353
+ `Agent${i}`,
354
+ { taskDescription: `Agent ${i}` },
355
+ TEST_PROJECT_PATH,
356
+ );
357
+ }
358
+
359
+ // Manually corrupt the views
360
+ const db = await getDatabase(TEST_PROJECT_PATH);
361
+ await db.query("DELETE FROM agents WHERE project_key = 'test-project'");
362
+
363
+ // Verify views are empty
364
+ const empty = await db.query<{ count: string }>(
365
+ "SELECT COUNT(*) as count FROM agents WHERE project_key = 'test-project'",
366
+ );
367
+ expect(parseInt(empty.rows[0]?.count ?? "0")).toBe(0);
368
+
369
+ // Track progress
370
+ const progressUpdates: Array<{
371
+ processed: number;
372
+ total: number;
373
+ percent: number;
374
+ }> = [];
375
+
376
+ // Replay in batches of 10
377
+ const result = await replayEventsBatched(
378
+ "test-project",
379
+ async (_events, progress) => {
380
+ progressUpdates.push(progress);
381
+ },
382
+ { batchSize: 10, clearViews: false },
383
+ TEST_PROJECT_PATH,
384
+ );
385
+
386
+ // Verify all events replayed
387
+ expect(result.eventsReplayed).toBe(50);
388
+
389
+ // Verify progress updates
390
+ expect(progressUpdates.length).toBe(5); // 50 events / 10 per batch = 5 batches
391
+ expect(progressUpdates[0]).toMatchObject({
392
+ processed: 10,
393
+ total: 50,
394
+ percent: 20,
395
+ });
396
+ expect(progressUpdates[4]).toMatchObject({
397
+ processed: 50,
398
+ total: 50,
399
+ percent: 100,
400
+ });
401
+
402
+ // Verify views are restored
403
+ const restored = await db.query<{ count: string }>(
404
+ "SELECT COUNT(*) as count FROM agents WHERE project_key = 'test-project'",
405
+ );
406
+ expect(parseInt(restored.rows[0]?.count ?? "0")).toBe(50);
407
+ });
408
+
409
+ it("should handle zero events gracefully", async () => {
410
+ const progressUpdates: Array<{
411
+ processed: number;
412
+ total: number;
413
+ percent: number;
414
+ }> = [];
415
+
416
+ const result = await replayEventsBatched(
417
+ "test-project",
418
+ async (_events, progress) => {
419
+ progressUpdates.push(progress);
420
+ },
421
+ { batchSize: 10 },
422
+ TEST_PROJECT_PATH,
423
+ );
424
+
425
+ expect(result.eventsReplayed).toBe(0);
426
+ expect(progressUpdates.length).toBe(0);
427
+ });
428
+
429
+ it("should use custom batch size", async () => {
430
+ // Create 25 events
431
+ for (let i = 0; i < 25; i++) {
432
+ await registerAgent("test-project", `Agent${i}`, {}, TEST_PROJECT_PATH);
433
+ }
434
+
435
+ const progressUpdates: Array<{
436
+ processed: number;
437
+ total: number;
438
+ percent: number;
439
+ }> = [];
440
+
441
+ // Replay with batch size of 5
442
+ await replayEventsBatched(
443
+ "test-project",
444
+ async (_events, progress) => {
445
+ progressUpdates.push(progress);
446
+ },
447
+ { batchSize: 5, clearViews: false },
448
+ TEST_PROJECT_PATH,
449
+ );
450
+
451
+ // Should have 5 batches (25 events / 5 per batch)
452
+ expect(progressUpdates.length).toBe(5);
453
+ });
454
+ });
455
+
456
+ describe("getDatabaseStats", () => {
457
+ it("should return correct counts", async () => {
458
+ await registerAgent("test-project", "Agent1", {}, TEST_PROJECT_PATH);
459
+ await sendMessage(
460
+ "test-project",
461
+ "Agent1",
462
+ ["Agent1"],
463
+ "Test",
464
+ "Body",
465
+ {},
466
+ TEST_PROJECT_PATH,
467
+ );
468
+ await reserveFiles(
469
+ "test-project",
470
+ "Agent1",
471
+ ["src/**"],
472
+ {},
473
+ TEST_PROJECT_PATH,
474
+ );
475
+
476
+ const stats = await getDatabaseStats(TEST_PROJECT_PATH);
477
+
478
+ expect(stats.events).toBe(3); // register + message + reserve
479
+ expect(stats.agents).toBe(1);
480
+ expect(stats.messages).toBe(1);
481
+ expect(stats.reservations).toBe(1);
482
+ });
483
+ });
484
+
485
+ describe("SQL Injection Protection", () => {
486
+ it("should handle malicious projectKey in replayEvents with clearViews", async () => {
487
+ // Create a legitimate project first
488
+ await registerAgent("legit-project", "Agent1", {}, TEST_PROJECT_PATH);
489
+ await sendMessage(
490
+ "legit-project",
491
+ "Agent1",
492
+ ["Agent1"],
493
+ "Test",
494
+ "Body",
495
+ {},
496
+ TEST_PROJECT_PATH,
497
+ );
498
+
499
+ // Attempt SQL injection via projectKey
500
+ const maliciousKey = "'; DROP TABLE events; --";
501
+
502
+ // Should not throw, should not drop tables
503
+ await replayEvents(
504
+ { projectKey: maliciousKey, clearViews: true },
505
+ TEST_PROJECT_PATH,
506
+ );
507
+
508
+ // Verify events table still exists and legit data is intact
509
+ const events = await readEvents({}, TEST_PROJECT_PATH);
510
+ expect(events).toBeDefined();
511
+ expect(events.length).toBeGreaterThan(0);
512
+
513
+ // Verify legit project data still exists
514
+ const legitEvents = await readEvents(
515
+ { projectKey: "legit-project" },
516
+ TEST_PROJECT_PATH,
517
+ );
518
+ expect(legitEvents.length).toBeGreaterThan(0);
519
+ });
520
+
521
+ it("should handle malicious projectKey with UNION injection attempt", async () => {
522
+ await registerAgent("safe-project", "Agent1", {}, TEST_PROJECT_PATH);
523
+
524
+ const unionInjection = "' UNION SELECT * FROM agents --";
525
+
526
+ // Should treat the entire string as a literal projectKey
527
+ await replayEvents(
528
+ { projectKey: unionInjection, clearViews: true },
529
+ TEST_PROJECT_PATH,
530
+ );
531
+
532
+ // Verify safe-project data still exists
533
+ const events = await readEvents(
534
+ { projectKey: "safe-project" },
535
+ TEST_PROJECT_PATH,
536
+ );
537
+ expect(events.length).toBeGreaterThan(0);
538
+ });
539
+
540
+ it("should handle malicious projectKey in readEvents", async () => {
541
+ await registerAgent("test-project", "Agent1", {}, TEST_PROJECT_PATH);
542
+
543
+ const maliciousKey = "test' OR '1'='1";
544
+
545
+ // Should return no results (no project with that exact key)
546
+ const events = await readEvents(
547
+ { projectKey: maliciousKey },
548
+ TEST_PROJECT_PATH,
549
+ );
550
+
551
+ // Should not return all events (which would happen if injection succeeded)
552
+ expect(events.length).toBe(0);
553
+ });
554
+
555
+ it("should handle malicious projectKey in getLatestSequence", async () => {
556
+ await registerAgent("real-project", "Agent1", {}, TEST_PROJECT_PATH);
557
+
558
+ const maliciousKey = "'; DELETE FROM events WHERE '1'='1";
559
+
560
+ const seq = await getLatestSequence(maliciousKey, TEST_PROJECT_PATH);
561
+
562
+ // Should return 0 (no events for this malicious key)
563
+ expect(seq).toBe(0);
564
+
565
+ // Verify events table still has data
566
+ const allEvents = await readEvents({}, TEST_PROJECT_PATH);
567
+ expect(allEvents.length).toBeGreaterThan(0);
568
+ });
569
+
570
+ it("should handle special SQL characters in projectKey", async () => {
571
+ const specialCharsKey = "project'; SELECT * FROM events; --";
572
+
573
+ await registerAgent(specialCharsKey, "Agent1", {}, TEST_PROJECT_PATH);
574
+
575
+ // Should be able to read back with the exact key
576
+ const events = await readEvents(
577
+ { projectKey: specialCharsKey },
578
+ TEST_PROJECT_PATH,
579
+ );
580
+
581
+ expect(events.length).toBe(1);
582
+ expect(events[0]?.project_key).toBe(specialCharsKey);
583
+ });
584
+
585
+ it("should handle malicious agent names", async () => {
586
+ const maliciousName = "Agent1'; DROP TABLE agents; --";
587
+
588
+ await registerAgent("test-project", maliciousName, {}, TEST_PROJECT_PATH);
589
+
590
+ // Verify agent was created with the literal name
591
+ const db = await getDatabase(TEST_PROJECT_PATH);
592
+ const agents = await db.query<{ name: string }>(
593
+ "SELECT name FROM agents WHERE project_key = $1",
594
+ ["test-project"],
595
+ );
596
+
597
+ expect(agents.rows.length).toBe(1);
598
+ expect(agents.rows[0]?.name).toBe(maliciousName);
599
+
600
+ // Verify tables still exist
601
+ const events = await readEvents({}, TEST_PROJECT_PATH);
602
+ expect(events).toBeDefined();
603
+ });
604
+
605
+ it("should handle malicious message subjects and bodies", async () => {
606
+ await registerAgent("test-project", "Agent1", {}, TEST_PROJECT_PATH);
607
+
608
+ const maliciousSubject = "'; DELETE FROM messages WHERE '1'='1; --";
609
+ const maliciousBody =
610
+ "Body with SQL: '); DROP TABLE message_recipients; --";
611
+
612
+ await sendMessage(
613
+ "test-project",
614
+ "Agent1",
615
+ ["Agent1"],
616
+ maliciousSubject,
617
+ maliciousBody,
618
+ {},
619
+ TEST_PROJECT_PATH,
620
+ );
621
+
622
+ // Verify message was stored with literal values
623
+ const db = await getDatabase(TEST_PROJECT_PATH);
624
+ const messages = await db.query<{ subject: string; body: string }>(
625
+ "SELECT subject, body FROM messages WHERE project_key = $1",
626
+ ["test-project"],
627
+ );
628
+
629
+ expect(messages.rows.length).toBe(1);
630
+ expect(messages.rows[0]?.subject).toBe(maliciousSubject);
631
+ expect(messages.rows[0]?.body).toBe(maliciousBody);
632
+ });
633
+
634
+ it("should handle malicious file paths in reservations", async () => {
635
+ await registerAgent("test-project", "Agent1", {}, TEST_PROJECT_PATH);
636
+
637
+ const maliciousPath = "src/**'; DELETE FROM reservations WHERE '1'='1";
638
+
639
+ await reserveFiles(
640
+ "test-project",
641
+ "Agent1",
642
+ [maliciousPath],
643
+ {},
644
+ TEST_PROJECT_PATH,
645
+ );
646
+
647
+ // Verify reservation was created with literal path
648
+ const db = await getDatabase(TEST_PROJECT_PATH);
649
+ const reservations = await db.query<{ path_pattern: string }>(
650
+ "SELECT path_pattern FROM reservations WHERE project_key = $1",
651
+ ["test-project"],
652
+ );
653
+
654
+ expect(reservations.rows.length).toBe(1);
655
+ expect(reservations.rows[0]?.path_pattern).toBe(maliciousPath);
656
+ });
657
+ });
658
+ });