opencode-swarm-plugin 0.22.0 → 0.23.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 (128) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +109 -429
  4. package/dist/agent-mail.d.ts +480 -0
  5. package/dist/agent-mail.d.ts.map +1 -0
  6. package/dist/anti-patterns.d.ts +257 -0
  7. package/dist/anti-patterns.d.ts.map +1 -0
  8. package/dist/beads.d.ts +377 -0
  9. package/dist/beads.d.ts.map +1 -0
  10. package/dist/eval-capture.d.ts +206 -0
  11. package/dist/eval-capture.d.ts.map +1 -0
  12. package/dist/index.d.ts +1299 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +498 -4246
  15. package/dist/learning.d.ts +670 -0
  16. package/dist/learning.d.ts.map +1 -0
  17. package/dist/mandate-promotion.d.ts +93 -0
  18. package/dist/mandate-promotion.d.ts.map +1 -0
  19. package/dist/mandate-storage.d.ts +209 -0
  20. package/dist/mandate-storage.d.ts.map +1 -0
  21. package/dist/mandates.d.ts +230 -0
  22. package/dist/mandates.d.ts.map +1 -0
  23. package/dist/output-guardrails.d.ts +125 -0
  24. package/dist/output-guardrails.d.ts.map +1 -0
  25. package/dist/pattern-maturity.d.ts +246 -0
  26. package/dist/pattern-maturity.d.ts.map +1 -0
  27. package/dist/plugin.d.ts +22 -0
  28. package/dist/plugin.d.ts.map +1 -0
  29. package/dist/plugin.js +493 -4241
  30. package/dist/rate-limiter.d.ts +218 -0
  31. package/dist/rate-limiter.d.ts.map +1 -0
  32. package/dist/repo-crawl.d.ts +146 -0
  33. package/dist/repo-crawl.d.ts.map +1 -0
  34. package/dist/schemas/bead.d.ts +255 -0
  35. package/dist/schemas/bead.d.ts.map +1 -0
  36. package/dist/schemas/evaluation.d.ts +161 -0
  37. package/dist/schemas/evaluation.d.ts.map +1 -0
  38. package/dist/schemas/index.d.ts +34 -0
  39. package/dist/schemas/index.d.ts.map +1 -0
  40. package/dist/schemas/mandate.d.ts +336 -0
  41. package/dist/schemas/mandate.d.ts.map +1 -0
  42. package/dist/schemas/swarm-context.d.ts +131 -0
  43. package/dist/schemas/swarm-context.d.ts.map +1 -0
  44. package/dist/schemas/task.d.ts +188 -0
  45. package/dist/schemas/task.d.ts.map +1 -0
  46. package/dist/skills.d.ts +471 -0
  47. package/dist/skills.d.ts.map +1 -0
  48. package/dist/storage.d.ts +260 -0
  49. package/dist/storage.d.ts.map +1 -0
  50. package/dist/structured.d.ts +196 -0
  51. package/dist/structured.d.ts.map +1 -0
  52. package/dist/swarm-decompose.d.ts +201 -0
  53. package/dist/swarm-decompose.d.ts.map +1 -0
  54. package/dist/swarm-mail.d.ts +240 -0
  55. package/dist/swarm-mail.d.ts.map +1 -0
  56. package/dist/swarm-orchestrate.d.ts +708 -0
  57. package/dist/swarm-orchestrate.d.ts.map +1 -0
  58. package/dist/swarm-prompts.d.ts +292 -0
  59. package/dist/swarm-prompts.d.ts.map +1 -0
  60. package/dist/swarm-strategies.d.ts +100 -0
  61. package/dist/swarm-strategies.d.ts.map +1 -0
  62. package/dist/swarm.d.ts +455 -0
  63. package/dist/swarm.d.ts.map +1 -0
  64. package/dist/tool-availability.d.ts +91 -0
  65. package/dist/tool-availability.d.ts.map +1 -0
  66. package/docs/planning/ADR-001-monorepo-structure.md +171 -0
  67. package/docs/planning/ADR-002-package-extraction.md +393 -0
  68. package/docs/planning/ADR-003-performance-improvements.md +451 -0
  69. package/docs/planning/ADR-004-message-queue-features.md +187 -0
  70. package/docs/planning/ADR-005-devtools-observability.md +202 -0
  71. package/docs/planning/ROADMAP.md +368 -0
  72. package/package.json +13 -24
  73. package/src/agent-mail.ts +1 -1
  74. package/src/beads.ts +1 -2
  75. package/src/index.ts +2 -2
  76. package/src/learning.integration.test.ts +66 -11
  77. package/src/mandate-storage.test.ts +3 -3
  78. package/src/storage.ts +78 -10
  79. package/src/swarm-mail.ts +3 -3
  80. package/src/swarm-orchestrate.ts +7 -7
  81. package/src/tool-availability.ts +1 -1
  82. package/tsconfig.json +1 -1
  83. package/.beads/.local_version +0 -1
  84. package/.beads/README.md +0 -81
  85. package/.beads/analysis/skill-architecture-meta-skills.md +0 -1562
  86. package/.beads/config.yaml +0 -62
  87. package/.beads/issues.jsonl +0 -2197
  88. package/.beads/metadata.json +0 -4
  89. package/.gitattributes +0 -3
  90. package/.github/workflows/ci.yml +0 -30
  91. package/.github/workflows/opencode.yml +0 -31
  92. package/.opencode/skills/tdd/SKILL.md +0 -182
  93. package/INTEGRATION_EXAMPLE.md +0 -66
  94. package/VERIFICATION_QUALITY_PATTERNS.md +0 -565
  95. package/bun.lock +0 -286
  96. package/dist/pglite.data +0 -0
  97. package/dist/pglite.wasm +0 -0
  98. package/src/streams/agent-mail.test.ts +0 -777
  99. package/src/streams/agent-mail.ts +0 -535
  100. package/src/streams/debug.test.ts +0 -500
  101. package/src/streams/debug.ts +0 -727
  102. package/src/streams/effect/ask.integration.test.ts +0 -314
  103. package/src/streams/effect/ask.ts +0 -202
  104. package/src/streams/effect/cursor.integration.test.ts +0 -418
  105. package/src/streams/effect/cursor.ts +0 -288
  106. package/src/streams/effect/deferred.test.ts +0 -357
  107. package/src/streams/effect/deferred.ts +0 -445
  108. package/src/streams/effect/index.ts +0 -17
  109. package/src/streams/effect/layers.ts +0 -73
  110. package/src/streams/effect/lock.test.ts +0 -385
  111. package/src/streams/effect/lock.ts +0 -399
  112. package/src/streams/effect/mailbox.test.ts +0 -260
  113. package/src/streams/effect/mailbox.ts +0 -318
  114. package/src/streams/events.test.ts +0 -924
  115. package/src/streams/events.ts +0 -329
  116. package/src/streams/index.test.ts +0 -229
  117. package/src/streams/index.ts +0 -578
  118. package/src/streams/migrations.test.ts +0 -359
  119. package/src/streams/migrations.ts +0 -362
  120. package/src/streams/projections.test.ts +0 -611
  121. package/src/streams/projections.ts +0 -504
  122. package/src/streams/store.integration.test.ts +0 -658
  123. package/src/streams/store.ts +0 -1075
  124. package/src/streams/swarm-mail.ts +0 -552
  125. package/test-bug-fixes.ts +0 -86
  126. package/vitest.integration.config.ts +0 -19
  127. package/vitest.integration.setup.ts +0 -48
  128. package/workflow-integration-analysis.md +0 -876
@@ -1,552 +0,0 @@
1
- /**
2
- * Swarm Mail - Embedded event-sourced implementation
3
- *
4
- * Replaces the MCP-based agent-mail with embedded PGLite storage.
5
- * Same API surface, but no external server dependency.
6
- *
7
- * Key features:
8
- * - Event sourcing for full audit trail
9
- * - Offset-based resumability (Durable Streams inspired)
10
- * - Materialized views for fast queries
11
- * - File reservation with conflict detection
12
- *
13
- * Effect-TS Integration:
14
- * - DurableMailbox for message send/receive (envelope pattern)
15
- * - DurableCursor for positioned inbox consumption with checkpointing
16
- * - DurableLock for file reservations (mutual exclusion via CAS)
17
- * - DurableDeferred for request/response messaging
18
- */
19
- import { createEvent } from "./events";
20
- import { isDatabaseHealthy, getDatabaseStats } from "./index";
21
- import {
22
- checkConflicts,
23
- getActiveReservations,
24
- getInbox,
25
- getMessage,
26
- } from "./projections";
27
- import { appendEvent, registerAgent, reserveFiles, sendMessage } from "./store";
28
-
29
- // ============================================================================
30
- // Constants
31
- // ============================================================================
32
-
33
- const MAX_INBOX_LIMIT = 5; // HARD CAP - context preservation
34
- const DEFAULT_TTL_SECONDS = 3600; // 1 hour
35
-
36
- // Agent name generation
37
- const ADJECTIVES = [
38
- "Blue",
39
- "Red",
40
- "Green",
41
- "Gold",
42
- "Silver",
43
- "Swift",
44
- "Bright",
45
- "Dark",
46
- "Calm",
47
- "Bold",
48
- "Wise",
49
- "Quick",
50
- "Warm",
51
- "Cool",
52
- "Pure",
53
- "Wild",
54
- ];
55
- const NOUNS = [
56
- "Lake",
57
- "Stone",
58
- "River",
59
- "Mountain",
60
- "Forest",
61
- "Ocean",
62
- "Star",
63
- "Moon",
64
- "Wind",
65
- "Fire",
66
- "Cloud",
67
- "Storm",
68
- "Dawn",
69
- "Dusk",
70
- "Hawk",
71
- "Wolf",
72
- ];
73
-
74
- function generateSwarmAgentName(): string {
75
- const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
76
- const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
77
- return `${adj}${noun}`;
78
- }
79
-
80
- // ============================================================================
81
- // Types
82
- // ============================================================================
83
-
84
- export interface SwarmMailContext {
85
- projectKey: string;
86
- agentName: string;
87
- }
88
-
89
- export interface InitSwarmAgentOptions {
90
- projectPath: string;
91
- agentName?: string;
92
- program?: string;
93
- model?: string;
94
- taskDescription?: string;
95
- }
96
-
97
- export interface SendSwarmMessageOptions {
98
- projectPath: string;
99
- fromAgent: string;
100
- toAgents: string[];
101
- subject: string;
102
- body: string;
103
- threadId?: string;
104
- importance?: "low" | "normal" | "high" | "urgent";
105
- ackRequired?: boolean;
106
- }
107
-
108
- export interface SendSwarmMessageResult {
109
- success: boolean;
110
- messageId: number;
111
- threadId?: string;
112
- recipientCount: number;
113
- }
114
-
115
- export interface GetSwarmInboxOptions {
116
- projectPath: string;
117
- agentName: string;
118
- limit?: number;
119
- urgentOnly?: boolean;
120
- unreadOnly?: boolean;
121
- includeBodies?: boolean;
122
- }
123
-
124
- export interface SwarmInboxMessage {
125
- id: number;
126
- from_agent: string;
127
- subject: string;
128
- body?: string;
129
- thread_id: string | null;
130
- importance: string;
131
- created_at: number;
132
- }
133
-
134
- export interface SwarmInboxResult {
135
- messages: SwarmInboxMessage[];
136
- total: number;
137
- }
138
-
139
- export interface ReadSwarmMessageOptions {
140
- projectPath: string;
141
- messageId: number;
142
- agentName?: string;
143
- markAsRead?: boolean;
144
- }
145
-
146
- export interface ReserveSwarmFilesOptions {
147
- projectPath: string;
148
- agentName: string;
149
- paths: string[];
150
- reason?: string;
151
- exclusive?: boolean;
152
- ttlSeconds?: number;
153
- force?: boolean;
154
- }
155
-
156
- export interface GrantedSwarmReservation {
157
- id: number;
158
- path_pattern: string;
159
- exclusive: boolean;
160
- expiresAt: number;
161
- }
162
-
163
- export interface SwarmReservationConflict {
164
- path: string;
165
- holder: string;
166
- pattern: string;
167
- }
168
-
169
- export interface ReserveSwarmFilesResult {
170
- granted: GrantedSwarmReservation[];
171
- conflicts: SwarmReservationConflict[];
172
- }
173
-
174
- export interface ReleaseSwarmFilesOptions {
175
- projectPath: string;
176
- agentName: string;
177
- paths?: string[];
178
- reservationIds?: number[];
179
- }
180
-
181
- export interface ReleaseSwarmFilesResult {
182
- released: number;
183
- releasedAt: number;
184
- }
185
-
186
- export interface AcknowledgeSwarmOptions {
187
- projectPath: string;
188
- messageId: number;
189
- agentName: string;
190
- }
191
-
192
- export interface AcknowledgeSwarmResult {
193
- acknowledged: boolean;
194
- acknowledgedAt: string | null;
195
- }
196
-
197
- export interface SwarmHealthResult {
198
- healthy: boolean;
199
- database: "connected" | "disconnected";
200
- stats?: {
201
- events: number;
202
- agents: number;
203
- messages: number;
204
- reservations: number;
205
- };
206
- }
207
-
208
- // ============================================================================
209
- // Agent Operations
210
- // ============================================================================
211
-
212
- /**
213
- * Initialize a swarm agent for this session
214
- *
215
- * Future: Can use DurableMailbox.create() for actor-style message consumption
216
- */
217
- export async function initSwarmAgent(
218
- options: InitSwarmAgentOptions,
219
- ): Promise<SwarmMailContext> {
220
- const {
221
- projectPath,
222
- agentName = generateSwarmAgentName(),
223
- program = "opencode",
224
- model = "unknown",
225
- taskDescription,
226
- } = options;
227
-
228
- // Register the agent (creates event + updates view)
229
- await registerAgent(
230
- projectPath, // Use projectPath as projectKey
231
- agentName,
232
- { program, model, taskDescription },
233
- projectPath,
234
- );
235
-
236
- return {
237
- projectKey: projectPath,
238
- agentName,
239
- };
240
- }
241
-
242
- // ============================================================================
243
- // Message Operations
244
- // ============================================================================
245
-
246
- /**
247
- * Send a message to other swarm agents
248
- *
249
- * Future: Use DurableMailbox.send() for envelope pattern with replyTo support
250
- */
251
- export async function sendSwarmMessage(
252
- options: SendSwarmMessageOptions,
253
- ): Promise<SendSwarmMessageResult> {
254
- const {
255
- projectPath,
256
- fromAgent,
257
- toAgents,
258
- subject,
259
- body,
260
- threadId,
261
- importance = "normal",
262
- ackRequired = false,
263
- } = options;
264
-
265
- await sendMessage(
266
- projectPath,
267
- fromAgent,
268
- toAgents,
269
- subject,
270
- body,
271
- { threadId, importance, ackRequired },
272
- projectPath,
273
- );
274
-
275
- // Get the message ID from the messages table (not the event ID)
276
- const { getDatabase } = await import("./index");
277
- const db = await getDatabase(projectPath);
278
- const result = await db.query<{ id: number }>(
279
- `SELECT id FROM messages
280
- WHERE project_key = $1 AND from_agent = $2 AND subject = $3
281
- ORDER BY created_at DESC LIMIT 1`,
282
- [projectPath, fromAgent, subject],
283
- );
284
-
285
- const messageId = result.rows[0]?.id ?? 0;
286
-
287
- return {
288
- success: true,
289
- messageId,
290
- threadId,
291
- recipientCount: toAgents.length,
292
- };
293
- }
294
-
295
- /**
296
- * Get inbox messages for a swarm agent
297
- *
298
- * Future: Use DurableCursor.consume() for positioned consumption with checkpointing
299
- */
300
- export async function getSwarmInbox(
301
- options: GetSwarmInboxOptions,
302
- ): Promise<SwarmInboxResult> {
303
- const {
304
- projectPath,
305
- agentName,
306
- limit = MAX_INBOX_LIMIT,
307
- urgentOnly = false,
308
- unreadOnly = false,
309
- includeBodies = false,
310
- } = options;
311
-
312
- // Enforce max limit
313
- const effectiveLimit = Math.min(limit, MAX_INBOX_LIMIT);
314
-
315
- const messages = await getInbox(
316
- projectPath,
317
- agentName,
318
- {
319
- limit: effectiveLimit,
320
- urgentOnly,
321
- unreadOnly,
322
- includeBodies,
323
- },
324
- projectPath,
325
- );
326
-
327
- return {
328
- messages: messages.map((m) => ({
329
- id: m.id,
330
- from_agent: m.from_agent,
331
- subject: m.subject,
332
- body: includeBodies ? m.body : undefined,
333
- thread_id: m.thread_id,
334
- importance: m.importance,
335
- created_at: m.created_at,
336
- })),
337
- total: messages.length,
338
- };
339
- }
340
-
341
- /**
342
- * Read a single message with full body
343
- */
344
- export async function readSwarmMessage(
345
- options: ReadSwarmMessageOptions,
346
- ): Promise<SwarmInboxMessage | null> {
347
- const { projectPath, messageId, agentName, markAsRead = false } = options;
348
-
349
- const message = await getMessage(projectPath, messageId, projectPath);
350
-
351
- if (!message) {
352
- return null;
353
- }
354
-
355
- // Mark as read if requested
356
- if (markAsRead && agentName) {
357
- await appendEvent(
358
- createEvent("message_read", {
359
- project_key: projectPath,
360
- message_id: messageId,
361
- agent_name: agentName,
362
- }),
363
- projectPath,
364
- );
365
- }
366
-
367
- return {
368
- id: message.id,
369
- from_agent: message.from_agent,
370
- subject: message.subject,
371
- body: message.body,
372
- thread_id: message.thread_id,
373
- importance: message.importance,
374
- created_at: message.created_at,
375
- };
376
- }
377
-
378
- // ============================================================================
379
- // Reservation Operations
380
- // ============================================================================
381
-
382
- /**
383
- * Reserve files for exclusive editing
384
- *
385
- * Always grants reservations (even with conflicts) - conflicts are warnings, not blockers.
386
- * This matches the test expectations and allows agents to proceed with awareness.
387
- *
388
- * Future: Use DurableLock.acquire() for distributed mutex with automatic expiry
389
- */
390
- export async function reserveSwarmFiles(
391
- options: ReserveSwarmFilesOptions,
392
- ): Promise<ReserveSwarmFilesResult> {
393
- const {
394
- projectPath,
395
- agentName,
396
- paths,
397
- reason,
398
- exclusive = true,
399
- ttlSeconds = DEFAULT_TTL_SECONDS,
400
- } = options;
401
-
402
- // Check for conflicts first
403
- const conflicts = await checkConflicts(
404
- projectPath,
405
- agentName,
406
- paths,
407
- projectPath,
408
- );
409
-
410
- // Always create reservations - conflicts are warnings, not blockers
411
- await reserveFiles(
412
- projectPath,
413
- agentName,
414
- paths,
415
- { reason, exclusive, ttlSeconds },
416
- projectPath,
417
- );
418
-
419
- // Query the actual reservation IDs from the database
420
- const reservations = await getActiveReservations(
421
- projectPath,
422
- projectPath,
423
- agentName,
424
- );
425
-
426
- // Filter to just the paths we reserved (most recent ones)
427
- const granted: GrantedSwarmReservation[] = reservations
428
- .filter((r) => paths.includes(r.path_pattern))
429
- .map((r) => ({
430
- id: r.id,
431
- path_pattern: r.path_pattern,
432
- exclusive: r.exclusive,
433
- expiresAt: r.expires_at,
434
- }));
435
-
436
- return {
437
- granted,
438
- conflicts: conflicts.map((c) => ({
439
- path: c.path,
440
- holder: c.holder,
441
- pattern: c.pattern,
442
- })),
443
- };
444
- }
445
-
446
- /**
447
- * Release file reservations
448
- *
449
- * Future: Use DurableLock.release() for automatic cleanup
450
- */
451
- export async function releaseSwarmFiles(
452
- options: ReleaseSwarmFilesOptions,
453
- ): Promise<ReleaseSwarmFilesResult> {
454
- const { projectPath, agentName, paths, reservationIds } = options;
455
-
456
- // Get current reservations to count what we're releasing
457
- const currentReservations = await getActiveReservations(
458
- projectPath,
459
- projectPath,
460
- agentName,
461
- );
462
-
463
- let releaseCount = 0;
464
-
465
- if (paths && paths.length > 0) {
466
- // Release specific paths
467
- releaseCount = currentReservations.filter((r) =>
468
- paths.includes(r.path_pattern),
469
- ).length;
470
- } else if (reservationIds && reservationIds.length > 0) {
471
- // Release by ID
472
- releaseCount = currentReservations.filter((r) =>
473
- reservationIds.includes(r.id),
474
- ).length;
475
- } else {
476
- // Release all
477
- releaseCount = currentReservations.length;
478
- }
479
-
480
- // Create release event
481
- await appendEvent(
482
- createEvent("file_released", {
483
- project_key: projectPath,
484
- agent_name: agentName,
485
- paths,
486
- reservation_ids: reservationIds,
487
- }),
488
- projectPath,
489
- );
490
-
491
- return {
492
- released: releaseCount,
493
- releasedAt: Date.now(),
494
- };
495
- }
496
-
497
- // ============================================================================
498
- // Acknowledgement Operations
499
- // ============================================================================
500
-
501
- /**
502
- * Acknowledge a swarm message
503
- */
504
- export async function acknowledgeSwarmMessage(
505
- options: AcknowledgeSwarmOptions,
506
- ): Promise<AcknowledgeSwarmResult> {
507
- const { projectPath, messageId, agentName } = options;
508
-
509
- const timestamp = Date.now();
510
-
511
- await appendEvent(
512
- createEvent("message_acked", {
513
- project_key: projectPath,
514
- message_id: messageId,
515
- agent_name: agentName,
516
- }),
517
- projectPath,
518
- );
519
-
520
- return {
521
- acknowledged: true,
522
- acknowledgedAt: new Date(timestamp).toISOString(),
523
- };
524
- }
525
-
526
- // ============================================================================
527
- // Health Check
528
- // ============================================================================
529
-
530
- /**
531
- * Check if the swarm mail store is healthy
532
- */
533
- export async function checkSwarmHealth(
534
- projectPath?: string,
535
- ): Promise<SwarmHealthResult> {
536
- const healthy = await isDatabaseHealthy(projectPath);
537
-
538
- if (!healthy) {
539
- return {
540
- healthy: false,
541
- database: "disconnected",
542
- };
543
- }
544
-
545
- const stats = await getDatabaseStats(projectPath);
546
-
547
- return {
548
- healthy: true,
549
- database: "connected",
550
- stats,
551
- };
552
- }
package/test-bug-fixes.ts DELETED
@@ -1,86 +0,0 @@
1
- /**
2
- * Quick test to verify bug fixes in store.ts
3
- *
4
- * Bug 1 (xcavl.5): Transaction rollback error propagation
5
- * Bug 2 (xcavl.6): File reservation idempotency
6
- */
7
-
8
- import { getDatabase, resetDatabase } from "./src/streams/index";
9
- import { reserveFiles } from "./src/streams/store";
10
-
11
- async function testBug1_RollbackErrorPropagation() {
12
- console.log("\n=== Testing Bug 1: Rollback Error Propagation ===");
13
- console.log(
14
- "Note: This is hard to test in isolation without forcing connection loss.",
15
- );
16
- console.log(
17
- "The fix ensures composite errors are thrown when both transaction AND rollback fail.",
18
- );
19
- console.log(
20
- "✅ Code review confirms fix is in place (see lines 141-166 in store.ts)",
21
- );
22
- }
23
-
24
- async function testBug2_ReservationIdempotency() {
25
- console.log("\n=== Testing Bug 2: File Reservation Idempotency ===");
26
-
27
- await resetDatabase(); // Clean slate
28
- const db = await getDatabase();
29
-
30
- const projectKey = "test-project";
31
- const agentName = "TestWorker";
32
- const paths = ["src/file.ts", "src/other.ts"];
33
-
34
- // First reservation
35
- await reserveFiles(projectKey, agentName, paths, {
36
- reason: "Initial reservation",
37
- exclusive: true,
38
- ttlSeconds: 3600,
39
- });
40
-
41
- // Query reservations
42
- const result1 = await db.query<{ count: string }>(
43
- `SELECT COUNT(*) as count FROM reservations
44
- WHERE project_key = $1 AND agent_name = $2 AND released_at IS NULL`,
45
- [projectKey, agentName],
46
- );
47
- const count1 = parseInt(result1.rows[0]?.count || "0");
48
- console.log(`After first reservation: ${count1} active reservations`);
49
-
50
- // RETRY the same reservation (simulating network timeout + retry)
51
- await reserveFiles(projectKey, agentName, paths, {
52
- reason: "Retry after timeout",
53
- exclusive: true,
54
- ttlSeconds: 3600,
55
- });
56
-
57
- // Query again - should still be 2 (idempotent)
58
- const result2 = await db.query<{ count: string }>(
59
- `SELECT COUNT(*) as count FROM reservations
60
- WHERE project_key = $1 AND agent_name = $2 AND released_at IS NULL`,
61
- [projectKey, agentName],
62
- );
63
- const count2 = parseInt(result2.rows[0]?.count || "0");
64
- console.log(`After retry: ${count2} active reservations`);
65
-
66
- if (count1 === count2 && count1 === 2) {
67
- console.log("✅ PASS: Reservation is idempotent (no duplicates created)");
68
- } else {
69
- console.log(
70
- `❌ FAIL: Expected 2 reservations both times, got ${count1} then ${count2}`,
71
- );
72
- }
73
- }
74
-
75
- async function main() {
76
- try {
77
- await testBug1_RollbackErrorPropagation();
78
- await testBug2_ReservationIdempotency();
79
- console.log("\n=== All tests complete ===\n");
80
- } catch (error) {
81
- console.error("Test failed:", error);
82
- process.exit(1);
83
- }
84
- }
85
-
86
- main();
@@ -1,19 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- include: ["src/**/*.integration.test.ts"],
6
- testTimeout: 30000, // Integration tests may be slower
7
- hookTimeout: 30000,
8
- // Run serially to avoid race conditions with shared services
9
- sequence: {
10
- concurrent: false,
11
- },
12
- env: {
13
- // Enable test-specific collections to isolate test data from production
14
- TEST_MEMORY_COLLECTIONS: "true",
15
- },
16
- // Global setup/teardown hooks
17
- globalSetup: "./vitest.integration.setup.ts",
18
- },
19
- });
@@ -1,48 +0,0 @@
1
- /**
2
- * Global setup/teardown for integration tests
3
- *
4
- * Ensures test-specific semantic-memory collections are cleaned up
5
- * after all integration tests complete.
6
- */
7
-
8
- export async function setup() {
9
- console.log("[vitest] Integration test setup: TEST_MEMORY_COLLECTIONS=true");
10
- // Setup runs before tests - environment variables are already set via vitest config
11
- }
12
-
13
- export async function teardown() {
14
- console.log(
15
- "[vitest] Integration test teardown: cleaning up test collections",
16
- );
17
-
18
- // Clean up test collections
19
- const testCollections = [
20
- "swarm-feedback-test",
21
- "swarm-patterns-test",
22
- "swarm-maturity-test",
23
- "swarm-maturity-test-feedback",
24
- ];
25
-
26
- for (const collection of testCollections) {
27
- try {
28
- // Attempt to remove test collection data
29
- // Note: semantic-memory doesn't have a built-in "delete collection" command,
30
- // so we'll use the remove command with a wildcard or rely on TTL/manual cleanup
31
- console.log(`[vitest] Attempting to clean collection: ${collection}`);
32
-
33
- // List items and remove them (semantic-memory may not support bulk delete)
34
- // This is a best-effort cleanup - some backends may require manual cleanup
35
- // For now, we'll just log that cleanup should happen
36
- console.log(
37
- `[vitest] Note: Collection "${collection}" may need manual cleanup via semantic-memory CLI`,
38
- );
39
- } catch (error) {
40
- console.warn(
41
- `[vitest] Failed to clean collection ${collection}:`,
42
- error instanceof Error ? error.message : String(error),
43
- );
44
- }
45
- }
46
-
47
- console.log("[vitest] Integration test teardown complete");
48
- }