opencode-swarm-plugin 0.18.0 → 0.20.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.
@@ -1,9 +1,44 @@
1
1
  /**
2
- * Schema Migration System for PGLite Event Store
2
+ * Schema Migration System
3
3
  *
4
- * Version-based migrations with up/down support.
5
- * Tracks applied migrations in schema_version table.
6
- * Idempotent - safe to run multiple times.
4
+ * Handles database schema evolution for the PGLite event store.
5
+ *
6
+ * ## How It Works
7
+ *
8
+ * 1. Each migration has a unique version number (incrementing integer)
9
+ * 2. On startup, `runMigrations()` checks current schema version
10
+ * 3. Migrations are applied in order until schema is current
11
+ * 4. Version is stored in `schema_version` table
12
+ *
13
+ * ## Adding a New Migration
14
+ *
15
+ * ```typescript
16
+ * // In migrations.ts
17
+ * export const migrations: Migration[] = [
18
+ * // ... existing migrations
19
+ * {
20
+ * version: 3,
21
+ * description: "add_new_column",
22
+ * up: `ALTER TABLE events ADD COLUMN new_col TEXT`,
23
+ * down: `ALTER TABLE events DROP COLUMN new_col`,
24
+ * },
25
+ * ];
26
+ * ```
27
+ *
28
+ * ## Rollback
29
+ *
30
+ * Rollback is supported via `rollbackTo(db, targetVersion)`.
31
+ * Note: Some migrations may not be fully reversible (data loss).
32
+ *
33
+ * ## Best Practices
34
+ *
35
+ * - Always test migrations on a copy of production data
36
+ * - Keep migrations small and focused
37
+ * - Include both `up` and `down` SQL
38
+ * - Use transactions for multi-statement migrations
39
+ * - Document any data transformations
40
+ *
41
+ * @module migrations
7
42
  */
8
43
  import type { PGlite } from "@electric-sql/pglite";
9
44
 
@@ -11,10 +46,17 @@ import type { PGlite } from "@electric-sql/pglite";
11
46
  // Types
12
47
  // ============================================================================
13
48
 
49
+ /**
50
+ * A database migration definition.
51
+ */
14
52
  export interface Migration {
53
+ /** Unique version number (must be sequential) */
15
54
  version: number;
55
+ /** Human-readable migration description */
16
56
  description: string;
57
+ /** SQL to apply the migration */
17
58
  up: string;
59
+ /** SQL to rollback the migration (best effort) */
18
60
  down: string;
19
61
  }
20
62
 
@@ -17,6 +17,7 @@ import {
17
17
  readEvents,
18
18
  getLatestSequence,
19
19
  replayEvents,
20
+ replayEventsBatched,
20
21
  registerAgent,
21
22
  sendMessage,
22
23
  reserveFiles,
@@ -343,6 +344,115 @@ describe("Event Store", () => {
343
344
  });
344
345
  });
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
+
346
456
  describe("getDatabaseStats", () => {
347
457
  it("should return correct counts", async () => {
348
458
  await registerAgent("test-project", "Agent1", {}, TEST_PROJECT_PATH);