@rudderhq/db 0.4.6-canary.9 → 0.5.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 (74) hide show
  1. package/dist/client.test.js +629 -0
  2. package/dist/client.test.js.map +1 -1
  3. package/dist/embedded-postgres-recovery.d.ts +1 -0
  4. package/dist/embedded-postgres-recovery.d.ts.map +1 -1
  5. package/dist/embedded-postgres-recovery.js +33 -0
  6. package/dist/embedded-postgres-recovery.js.map +1 -1
  7. package/dist/embedded-postgres-recovery.test.js +19 -1
  8. package/dist/embedded-postgres-recovery.test.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/migration-runtime.d.ts.map +1 -1
  14. package/dist/migration-runtime.js +3 -20
  15. package/dist/migration-runtime.js.map +1 -1
  16. package/dist/migrations/0102_complex_retro_girl.sql +38 -0
  17. package/dist/migrations/0103_cute_colonel_america.sql +4 -0
  18. package/dist/migrations/0104_unusual_mister_fear.sql +173 -0
  19. package/dist/migrations/0105_chat_queue_actor_reconciliation.sql +54 -0
  20. package/dist/migrations/0106_damp_amphibian.sql +9 -0
  21. package/dist/migrations/meta/0102_snapshot.json +19090 -0
  22. package/dist/migrations/meta/0103_snapshot.json +19114 -0
  23. package/dist/migrations/meta/0104_snapshot.json +20338 -0
  24. package/dist/migrations/meta/0105_snapshot.json +20338 -0
  25. package/dist/migrations/meta/0106_snapshot.json +20442 -0
  26. package/dist/migrations/meta/_journal.json +35 -0
  27. package/dist/schema/activity_log.d.ts +17 -0
  28. package/dist/schema/activity_log.d.ts.map +1 -1
  29. package/dist/schema/activity_log.js +6 -1
  30. package/dist/schema/activity_log.js.map +1 -1
  31. package/dist/schema/agent_wakeup_requests.d.ts.map +1 -1
  32. package/dist/schema/agent_wakeup_requests.js +5 -1
  33. package/dist/schema/agent_wakeup_requests.js.map +1 -1
  34. package/dist/schema/chat_control_actions.d.ts +442 -0
  35. package/dist/schema/chat_control_actions.d.ts.map +1 -0
  36. package/dist/schema/chat_control_actions.js +35 -0
  37. package/dist/schema/chat_control_actions.js.map +1 -0
  38. package/dist/schema/chat_conversations.d.ts +119 -0
  39. package/dist/schema/chat_conversations.d.ts.map +1 -1
  40. package/dist/schema/chat_conversations.js +10 -1
  41. package/dist/schema/chat_conversations.js.map +1 -1
  42. package/dist/schema/chat_generation_events.d.ts +268 -0
  43. package/dist/schema/chat_generation_events.d.ts.map +1 -0
  44. package/dist/schema/chat_generation_events.js +32 -0
  45. package/dist/schema/chat_generation_events.js.map +1 -0
  46. package/dist/schema/chat_generation_terminal_outbox.d.ts +370 -0
  47. package/dist/schema/chat_generation_terminal_outbox.d.ts.map +1 -0
  48. package/dist/schema/chat_generation_terminal_outbox.js +33 -0
  49. package/dist/schema/chat_generation_terminal_outbox.js.map +1 -0
  50. package/dist/schema/chat_generations.d.ts +279 -2
  51. package/dist/schema/chat_generations.d.ts.map +1 -1
  52. package/dist/schema/chat_generations.js +23 -2
  53. package/dist/schema/chat_generations.js.map +1 -1
  54. package/dist/schema/chat_queued_messages.d.ts +285 -2
  55. package/dist/schema/chat_queued_messages.d.ts.map +1 -1
  56. package/dist/schema/chat_queued_messages.js +23 -1
  57. package/dist/schema/chat_queued_messages.js.map +1 -1
  58. package/dist/schema/cost_events.d.ts +34 -0
  59. package/dist/schema/cost_events.d.ts.map +1 -1
  60. package/dist/schema/cost_events.js +5 -1
  61. package/dist/schema/cost_events.js.map +1 -1
  62. package/dist/schema/heartbeat_run_events.d.ts +17 -0
  63. package/dist/schema/heartbeat_run_events.d.ts.map +1 -1
  64. package/dist/schema/heartbeat_run_events.js +6 -1
  65. package/dist/schema/heartbeat_run_events.js.map +1 -1
  66. package/dist/schema/heartbeat_runs.d.ts +303 -0
  67. package/dist/schema/heartbeat_runs.d.ts.map +1 -1
  68. package/dist/schema/heartbeat_runs.js +24 -2
  69. package/dist/schema/heartbeat_runs.js.map +1 -1
  70. package/dist/schema/index.d.ts +3 -0
  71. package/dist/schema/index.d.ts.map +1 -1
  72. package/dist/schema/index.js +3 -0
  73. package/dist/schema/index.js.map +1 -1
  74. package/package.json +2 -2
@@ -1,3 +1,5 @@
1
+ import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
2
+ import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
1
3
  import { createHash } from "node:crypto";
2
4
  import fs from "node:fs";
3
5
  import net from "node:net";
@@ -62,6 +64,93 @@ async function migrationHash(migrationFile) {
62
64
  const content = await fs.promises.readFile(new URL(`./migrations/${migrationFile}`, import.meta.url), "utf8");
63
65
  return createHash("sha256").update(content).digest("hex");
64
66
  }
67
+ function createLegacyTerminalMigrationsFolder() {
68
+ const migrationsFolder = fs.mkdtempSync(path.join(os.tmpdir(), "rudder-legacy-terminal-migrations-"));
69
+ tempPaths.push(migrationsFolder);
70
+ fs.mkdirSync(path.join(migrationsFolder, "meta"));
71
+ const currentMigrationsUrl = new URL("./migrations/", import.meta.url);
72
+ const currentJournal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", currentMigrationsUrl), "utf8"));
73
+ const baseEntries = currentJournal.entries.filter((entry) => entry.idx < 102);
74
+ if (baseEntries.at(-1)?.tag !== "0101_bizarre_morlun") {
75
+ throw new Error("Legacy terminal migration fixture requires the 0101 base schema");
76
+ }
77
+ for (const entry of baseEntries) {
78
+ fs.copyFileSync(new URL(`${entry.tag}.sql`, currentMigrationsUrl), path.join(migrationsFolder, `${entry.tag}.sql`));
79
+ }
80
+ const fixtureUrl = new URL("./test-fixtures/legacy-terminal-state-migrations/", import.meta.url);
81
+ const manifest = JSON.parse(fs.readFileSync(new URL("manifest.json", fixtureUrl), "utf8"));
82
+ for (const entry of manifest.entries) {
83
+ const fixtureContent = fs.readFileSync(new URL(`${entry.tag}.sql`, fixtureUrl), "utf8");
84
+ const normalizedFixtureContent = fixtureContent.replaceAll("\r\n", "\n");
85
+ const historicalContent = normalizedFixtureContent.endsWith("\n")
86
+ ? normalizedFixtureContent.slice(0, -1)
87
+ : normalizedFixtureContent;
88
+ const actualHash = createHash("sha256").update(historicalContent).digest("hex");
89
+ if (actualHash !== entry.sha256) {
90
+ throw new Error(`Legacy migration fixture hash mismatch for ${entry.tag}: expected ${entry.sha256}, received ${actualHash}`);
91
+ }
92
+ fs.writeFileSync(path.join(migrationsFolder, `${entry.tag}.sql`), historicalContent);
93
+ }
94
+ fs.writeFileSync(path.join(migrationsFolder, "meta", "_journal.json"), JSON.stringify({
95
+ version: currentJournal.version,
96
+ dialect: currentJournal.dialect,
97
+ entries: [
98
+ ...baseEntries,
99
+ ...manifest.entries.map(({ sha256: _sha256, ...entry }) => entry),
100
+ ],
101
+ }));
102
+ return { manifest, migrationsFolder };
103
+ }
104
+ function createLegacyChatRuntimeControlsMigrationsFolder() {
105
+ const migrationsFolder = fs.mkdtempSync(path.join(os.tmpdir(), "rudder-legacy-chat-controls-migrations-"));
106
+ tempPaths.push(migrationsFolder);
107
+ fs.mkdirSync(path.join(migrationsFolder, "meta"));
108
+ const currentMigrationsUrl = new URL("./migrations/", import.meta.url);
109
+ const currentJournal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", currentMigrationsUrl), "utf8"));
110
+ const baseEntries = currentJournal.entries.filter((entry) => entry.idx < 102);
111
+ if (baseEntries.at(-1)?.tag !== "0101_bizarre_morlun") {
112
+ throw new Error("Legacy chat control migration fixture requires the 0101 base schema");
113
+ }
114
+ for (const entry of baseEntries) {
115
+ fs.copyFileSync(new URL(`${entry.tag}.sql`, currentMigrationsUrl), path.join(migrationsFolder, `${entry.tag}.sql`));
116
+ }
117
+ const historicalContent = fs.readFileSync(new URL("0104_unusual_mister_fear.sql", currentMigrationsUrl), "utf8");
118
+ const historicalHash = createHash("sha256").update(historicalContent).digest("hex");
119
+ if (historicalHash !== "a1fc0446af5ec1640890bb9cf36208eab8dce6687c233029bd54e179613e1af7") {
120
+ throw new Error(`Legacy chat runtime controls fixture hash changed: ${historicalHash}`);
121
+ }
122
+ const legacyEntry = {
123
+ idx: 102,
124
+ version: "7",
125
+ when: 1784159513933,
126
+ tag: "0102_chat_runtime_controls",
127
+ breakpoints: true,
128
+ };
129
+ fs.writeFileSync(path.join(migrationsFolder, `${legacyEntry.tag}.sql`), historicalContent);
130
+ fs.writeFileSync(path.join(migrationsFolder, "meta", "_journal.json"), JSON.stringify({
131
+ version: currentJournal.version,
132
+ dialect: currentJournal.dialect,
133
+ entries: [...baseEntries, legacyEntry],
134
+ }));
135
+ return { historicalHash, migrationsFolder };
136
+ }
137
+ function createCurrentMigrationsFolderThrough(maxIdx) {
138
+ const migrationsFolder = fs.mkdtempSync(path.join(os.tmpdir(), `rudder-migrations-through-${maxIdx}-`));
139
+ tempPaths.push(migrationsFolder);
140
+ fs.mkdirSync(path.join(migrationsFolder, "meta"));
141
+ const currentMigrationsUrl = new URL("./migrations/", import.meta.url);
142
+ const currentJournal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", currentMigrationsUrl), "utf8"));
143
+ const entries = currentJournal.entries.filter((entry) => entry.idx <= maxIdx);
144
+ for (const entry of entries) {
145
+ fs.copyFileSync(new URL(`${entry.tag}.sql`, currentMigrationsUrl), path.join(migrationsFolder, `${entry.tag}.sql`));
146
+ }
147
+ fs.writeFileSync(path.join(migrationsFolder, "meta", "_journal.json"), JSON.stringify({
148
+ version: currentJournal.version,
149
+ dialect: currentJournal.dialect,
150
+ entries,
151
+ }));
152
+ return migrationsFolder;
153
+ }
65
154
  afterEach(async () => {
66
155
  while (runningInstances.length > 0) {
67
156
  const instance = runningInstances.pop();
@@ -245,6 +334,546 @@ describe("applyPendingMigrations", () => {
245
334
  await verifySql.end();
246
335
  }
247
336
  }, 20_000);
337
+ it("reapplies consolidated migration 0102 over the legacy 0102-0104 feature shape", async () => {
338
+ const connectionString = await createTempDatabase();
339
+ const { manifest, migrationsFolder } = createLegacyTerminalMigrationsFolder();
340
+ expect(manifest.sourceCommit).toBe("0491a75db25a24e644e67a502239fd1209840f40");
341
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
342
+ try {
343
+ await migratePg(drizzlePg(sql), { migrationsFolder });
344
+ const legacyHistory = await sql.unsafe(`
345
+ SELECT hash, created_at
346
+ FROM "drizzle"."__drizzle_migrations"
347
+ ORDER BY created_at DESC
348
+ LIMIT 3
349
+ `);
350
+ expect(legacyHistory.map((row) => ({
351
+ hash: row.hash,
352
+ createdAt: Number(row.created_at),
353
+ }))).toEqual([...manifest.entries].reverse().map((entry) => ({
354
+ hash: entry.sha256,
355
+ createdAt: entry.when,
356
+ })));
357
+ const legacyColumns = await sql.unsafe(`
358
+ SELECT table_name, column_name
359
+ FROM information_schema.columns
360
+ WHERE table_schema = 'public'
361
+ AND table_name IN ('heartbeat_runs', 'cost_events')
362
+ AND column_name IN (
363
+ 'terminal_effects_pending',
364
+ 'process_exited_at',
365
+ 'terminal_effects_json',
366
+ 'terminal_effects_last_error',
367
+ 'budget_evaluated_at',
368
+ 'execution_owner_token',
369
+ 'execution_lease_expires_at',
370
+ 'terminal_effects_completed_json'
371
+ )
372
+ ORDER BY table_name, column_name
373
+ `);
374
+ expect(legacyColumns).toEqual([
375
+ { table_name: "cost_events", column_name: "budget_evaluated_at" },
376
+ { table_name: "heartbeat_runs", column_name: "process_exited_at" },
377
+ { table_name: "heartbeat_runs", column_name: "terminal_effects_json" },
378
+ { table_name: "heartbeat_runs", column_name: "terminal_effects_last_error" },
379
+ { table_name: "heartbeat_runs", column_name: "terminal_effects_pending" },
380
+ ]);
381
+ }
382
+ finally {
383
+ await sql.end();
384
+ }
385
+ const pendingState = await inspectMigrations(connectionString);
386
+ expect(pendingState).toMatchObject({
387
+ status: "needsMigrations",
388
+ pendingMigrations: [
389
+ "0055_illegal_sheva_callister.sql",
390
+ "0102_complex_retro_girl.sql",
391
+ "0103_cute_colonel_america.sql",
392
+ "0104_unusual_mister_fear.sql",
393
+ "0105_chat_queue_actor_reconciliation.sql",
394
+ "0106_damp_amphibian.sql",
395
+ ],
396
+ reason: "pending-migrations",
397
+ });
398
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
399
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
400
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
401
+ try {
402
+ const consolidatedHash = await migrationHash("0102_complex_retro_girl.sql");
403
+ const consolidatedHistory = await verifySql.unsafe(`
404
+ SELECT hash
405
+ FROM "drizzle"."__drizzle_migrations"
406
+ WHERE hash = '${consolidatedHash}'
407
+ `);
408
+ expect(consolidatedHistory).toEqual([{ hash: consolidatedHash }]);
409
+ const columns = await verifySql.unsafe(`
410
+ SELECT column_name
411
+ FROM information_schema.columns
412
+ WHERE table_schema = 'public'
413
+ AND table_name = 'heartbeat_runs'
414
+ AND column_name IN (
415
+ 'execution_owner_token',
416
+ 'execution_lease_expires_at',
417
+ 'session_params_after_json',
418
+ 'session_params_before_json',
419
+ 'session_reuse_scope',
420
+ 'terminal_effects_completed_json',
421
+ 'terminal_effects_last_error'
422
+ )
423
+ ORDER BY column_name
424
+ `);
425
+ expect(columns.map((row) => row.column_name)).toEqual([
426
+ "execution_lease_expires_at",
427
+ "execution_owner_token",
428
+ "session_params_after_json",
429
+ "session_params_before_json",
430
+ "session_reuse_scope",
431
+ "terminal_effects_completed_json",
432
+ "terminal_effects_last_error",
433
+ ]);
434
+ const indexes = await verifySql.unsafe(`
435
+ SELECT indexname
436
+ FROM pg_indexes
437
+ WHERE schemaname = 'public'
438
+ AND indexname IN (
439
+ 'heartbeat_runs_active_chat_conversation_uq',
440
+ 'heartbeat_runs_status_execution_lease_created_idx',
441
+ 'heartbeat_run_events_run_idempotency_key_uq'
442
+ )
443
+ ORDER BY indexname
444
+ `);
445
+ expect(indexes.map((row) => row.indexname)).toEqual([
446
+ "heartbeat_run_events_run_idempotency_key_uq",
447
+ "heartbeat_runs_active_chat_conversation_uq",
448
+ "heartbeat_runs_status_execution_lease_created_idx",
449
+ ]);
450
+ }
451
+ finally {
452
+ await verifySql.end();
453
+ }
454
+ }, 20_000);
455
+ it("upgrades databases that applied the legacy 0102 chat runtime controls migration", async () => {
456
+ const connectionString = await createTempDatabase();
457
+ const { historicalHash, migrationsFolder } = createLegacyChatRuntimeControlsMigrationsFolder();
458
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
459
+ try {
460
+ await migratePg(drizzlePg(sql), { migrationsFolder });
461
+ await sql.unsafe(`
462
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
463
+ VALUES (
464
+ '00000000-0000-0000-0000-000000000122',
465
+ 'legacy-chat-controls',
466
+ 'Legacy Chat Controls',
467
+ 'LCC'
468
+ )
469
+ `);
470
+ await sql.unsafe(`
471
+ INSERT INTO "chat_conversations" ("id", "org_id", "title")
472
+ VALUES (
473
+ '00000000-0000-0000-0000-000000000123',
474
+ '00000000-0000-0000-0000-000000000122',
475
+ 'Legacy chat runtime controls'
476
+ )
477
+ `);
478
+ await sql.unsafe(`
479
+ INSERT INTO "chat_queued_messages" (
480
+ "id",
481
+ "org_id",
482
+ "conversation_id",
483
+ "position",
484
+ "status",
485
+ "client_mutation_id",
486
+ "payload",
487
+ "delivery_intent",
488
+ "delivery_disposition"
489
+ )
490
+ VALUES (
491
+ '00000000-0000-0000-0000-000000000124',
492
+ '00000000-0000-0000-0000-000000000122',
493
+ '00000000-0000-0000-0000-000000000123',
494
+ 1,
495
+ 'continuation_pending',
496
+ 'legacy-wip-steer',
497
+ '{"body":"legacy WIP steer"}'::jsonb,
498
+ 'steer',
499
+ 'continuation_pending'
500
+ )
501
+ `);
502
+ }
503
+ finally {
504
+ await sql.end();
505
+ }
506
+ const pendingState = await inspectMigrations(connectionString);
507
+ expect(pendingState).toMatchObject({
508
+ status: "needsMigrations",
509
+ pendingMigrations: [
510
+ "0055_illegal_sheva_callister.sql",
511
+ "0102_complex_retro_girl.sql",
512
+ "0103_cute_colonel_america.sql",
513
+ "0105_chat_queue_actor_reconciliation.sql",
514
+ "0106_damp_amphibian.sql",
515
+ ],
516
+ reason: "pending-migrations",
517
+ });
518
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
519
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
520
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
521
+ try {
522
+ const legacyHistory = await verifySql.unsafe(`
523
+ SELECT hash
524
+ FROM "drizzle"."__drizzle_migrations"
525
+ WHERE hash = '${historicalHash}'
526
+ `);
527
+ expect(legacyHistory).toEqual([{ hash: historicalHash }]);
528
+ const [queueItem] = await verifySql.unsafe(`
529
+ SELECT
530
+ status,
531
+ delivery_intent,
532
+ delivery_disposition,
533
+ request_actor,
534
+ reconciliation_reason
535
+ FROM "chat_queued_messages"
536
+ WHERE "id" = '00000000-0000-0000-0000-000000000124'
537
+ `);
538
+ expect(queueItem).toEqual({
539
+ status: "queued",
540
+ delivery_intent: "queue",
541
+ delivery_disposition: null,
542
+ request_actor: null,
543
+ reconciliation_reason: "legacy_request_actor_unavailable",
544
+ });
545
+ }
546
+ finally {
547
+ await verifySql.end();
548
+ }
549
+ }, 20_000);
550
+ it("upgrades 0102 heartbeat runs with durable session lineage columns", async () => {
551
+ const connectionString = await createTempDatabase();
552
+ const migrationsThrough0102 = createCurrentMigrationsFolderThrough(102);
553
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
554
+ try {
555
+ await migratePg(drizzlePg(sql), { migrationsFolder: migrationsThrough0102 });
556
+ await sql.unsafe(`
557
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
558
+ VALUES ('00000000-0000-0000-0000-000000000103', 'session-lineage', 'Session Lineage', 'SLG')
559
+ `);
560
+ await sql.unsafe(`
561
+ INSERT INTO "agents" ("id", "org_id", "name")
562
+ VALUES (
563
+ '00000000-0000-0000-0000-000000000104',
564
+ '00000000-0000-0000-0000-000000000103',
565
+ 'Migration Agent'
566
+ )
567
+ `);
568
+ await sql.unsafe(`
569
+ INSERT INTO "heartbeat_runs" ("id", "org_id", "agent_id")
570
+ VALUES (
571
+ '00000000-0000-0000-0000-000000000105',
572
+ '00000000-0000-0000-0000-000000000103',
573
+ '00000000-0000-0000-0000-000000000104'
574
+ )
575
+ `);
576
+ }
577
+ finally {
578
+ await sql.end();
579
+ }
580
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
581
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
582
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
583
+ try {
584
+ const [legacyRun] = await verifySql.unsafe(`
585
+ SELECT session_params_before_json, session_params_after_json, session_reuse_scope
586
+ FROM "heartbeat_runs"
587
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
588
+ `);
589
+ expect(legacyRun).toEqual({
590
+ session_params_before_json: null,
591
+ session_params_after_json: null,
592
+ session_reuse_scope: "unknown",
593
+ });
594
+ for (const scope of ["explicit", "task", "none", "unknown"]) {
595
+ await expect(verifySql.unsafe(`
596
+ UPDATE "heartbeat_runs"
597
+ SET "session_reuse_scope" = '${scope}',
598
+ "session_params_before_json" = '{"sessionId":"before"}'::jsonb,
599
+ "session_params_after_json" = '{"sessionId":"after"}'::jsonb
600
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
601
+ `)).resolves.toBeDefined();
602
+ }
603
+ await expect(verifySql.unsafe(`
604
+ UPDATE "heartbeat_runs"
605
+ SET "session_reuse_scope" = 'global'
606
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
607
+ `)).rejects.toMatchObject({ code: "23514" });
608
+ }
609
+ finally {
610
+ await verifySql.end();
611
+ }
612
+ }, 20_000);
613
+ it("reconciles legacy chat control states before creating active-generation constraints", async () => {
614
+ const connectionString = await createTempDatabase();
615
+ const migrationsThrough0103 = createCurrentMigrationsFolderThrough(103);
616
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
617
+ try {
618
+ await migratePg(drizzlePg(sql), { migrationsFolder: migrationsThrough0103 });
619
+ await sql.unsafe(`
620
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
621
+ VALUES (
622
+ '00000000-0000-0000-0000-000000000110',
623
+ 'chat-control-migration',
624
+ 'Chat Control Migration',
625
+ 'CCM'
626
+ )
627
+ `);
628
+ await sql.unsafe(`
629
+ INSERT INTO "agents" ("id", "org_id", "name")
630
+ VALUES (
631
+ '00000000-0000-0000-0000-000000000119',
632
+ '00000000-0000-0000-0000-000000000110',
633
+ 'Legacy queue agent'
634
+ )
635
+ `);
636
+ await sql.unsafe(`
637
+ INSERT INTO "chat_conversations" ("id", "org_id", "title")
638
+ VALUES (
639
+ '00000000-0000-0000-0000-000000000111',
640
+ '00000000-0000-0000-0000-000000000110',
641
+ 'Legacy chat control state'
642
+ )
643
+ `);
644
+ await sql.unsafe(`
645
+ INSERT INTO "chat_generations" ("id", "org_id", "conversation_id", "status")
646
+ VALUES
647
+ (
648
+ '00000000-0000-0000-0000-000000000112',
649
+ '00000000-0000-0000-0000-000000000110',
650
+ '00000000-0000-0000-0000-000000000111',
651
+ 'active'
652
+ ),
653
+ (
654
+ '00000000-0000-0000-0000-000000000113',
655
+ '00000000-0000-0000-0000-000000000110',
656
+ '00000000-0000-0000-0000-000000000111',
657
+ 'running'
658
+ )
659
+ `);
660
+ await sql.unsafe(`
661
+ INSERT INTO "chat_queued_messages" (
662
+ "id",
663
+ "org_id",
664
+ "conversation_id",
665
+ "position",
666
+ "status",
667
+ "client_mutation_id",
668
+ "payload",
669
+ "last_delivery_reason"
670
+ )
671
+ VALUES
672
+ (
673
+ '00000000-0000-0000-0000-000000000114',
674
+ '00000000-0000-0000-0000-000000000110',
675
+ '00000000-0000-0000-0000-000000000111',
676
+ 1,
677
+ 'steer_pending',
678
+ 'legacy-steer',
679
+ '{"body":"steer"}'::jsonb,
680
+ 'unsupported'
681
+ ),
682
+ (
683
+ '00000000-0000-0000-0000-000000000115',
684
+ '00000000-0000-0000-0000-000000000110',
685
+ '00000000-0000-0000-0000-000000000111',
686
+ 2,
687
+ 'dequeue_claimed',
688
+ 'legacy-claim',
689
+ '{"body":"claim"}'::jsonb,
690
+ NULL
691
+ ),
692
+ (
693
+ '00000000-0000-0000-0000-000000000116',
694
+ '00000000-0000-0000-0000-000000000110',
695
+ '00000000-0000-0000-0000-000000000111',
696
+ 3,
697
+ 'running',
698
+ 'legacy-running',
699
+ '{"body":"running"}'::jsonb,
700
+ NULL
701
+ ),
702
+ (
703
+ '00000000-0000-0000-0000-000000000117',
704
+ '00000000-0000-0000-0000-000000000110',
705
+ '00000000-0000-0000-0000-000000000111',
706
+ 4,
707
+ 'queued',
708
+ 'legacy-actorless-queue',
709
+ '{"body":"actorless queue"}'::jsonb,
710
+ NULL
711
+ ),
712
+ (
713
+ '00000000-0000-0000-0000-000000000118',
714
+ '00000000-0000-0000-0000-000000000110',
715
+ '00000000-0000-0000-0000-000000000111',
716
+ 5,
717
+ 'steer_pending',
718
+ 'legacy-actorless-steer',
719
+ '{"body":"actorless steer"}'::jsonb,
720
+ 'unsupported'
721
+ )
722
+ `);
723
+ await sql.unsafe(`
724
+ INSERT INTO "activity_log" (
725
+ "id",
726
+ "org_id",
727
+ "actor_type",
728
+ "actor_id",
729
+ "action",
730
+ "entity_type",
731
+ "entity_id",
732
+ "agent_id",
733
+ "details"
734
+ )
735
+ VALUES
736
+ (
737
+ '00000000-0000-0000-0000-000000000120',
738
+ '00000000-0000-0000-0000-000000000110',
739
+ 'user',
740
+ 'legacy-board-user',
741
+ 'chat.queue.created',
742
+ 'chat',
743
+ '00000000-0000-0000-0000-000000000111',
744
+ NULL,
745
+ '{"queuedMessageId":"00000000-0000-0000-0000-000000000114"}'::jsonb
746
+ ),
747
+ (
748
+ '00000000-0000-0000-0000-000000000121',
749
+ '00000000-0000-0000-0000-000000000110',
750
+ 'agent',
751
+ '00000000-0000-0000-0000-000000000119',
752
+ 'chat.queue.created',
753
+ 'chat',
754
+ '00000000-0000-0000-0000-000000000111',
755
+ '00000000-0000-0000-0000-000000000119',
756
+ '{"queuedMessageId":"00000000-0000-0000-0000-000000000115"}'::jsonb
757
+ )
758
+ `);
759
+ }
760
+ finally {
761
+ await sql.end();
762
+ }
763
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
764
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
765
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
766
+ try {
767
+ const generations = await verifySql.unsafe(`
768
+ SELECT
769
+ status,
770
+ terminal_reason,
771
+ control_state,
772
+ runtime_terminal_at IS NOT NULL AS runtime_terminal_recorded,
773
+ completed_at IS NOT NULL AS completed_recorded
774
+ FROM "chat_generations"
775
+ WHERE "conversation_id" = '00000000-0000-0000-0000-000000000111'
776
+ ORDER BY "id"
777
+ `);
778
+ expect(generations).toEqual([
779
+ {
780
+ status: "aborted",
781
+ terminal_reason: "ownerless_during_durable_control_migration",
782
+ control_state: "terminal",
783
+ runtime_terminal_recorded: true,
784
+ completed_recorded: true,
785
+ },
786
+ {
787
+ status: "aborted",
788
+ terminal_reason: "ownerless_during_durable_control_migration",
789
+ control_state: "terminal",
790
+ runtime_terminal_recorded: true,
791
+ completed_recorded: true,
792
+ },
793
+ ]);
794
+ const queueItems = await verifySql.unsafe(`
795
+ SELECT
796
+ position,
797
+ status,
798
+ delivery_intent,
799
+ delivery_disposition,
800
+ request_actor,
801
+ reconciliation_reason,
802
+ last_delivery_reason
803
+ FROM "chat_queued_messages"
804
+ WHERE "conversation_id" = '00000000-0000-0000-0000-000000000111'
805
+ ORDER BY "position"
806
+ `);
807
+ expect(queueItems).toEqual([
808
+ {
809
+ position: 1,
810
+ status: "continuation_pending",
811
+ delivery_intent: "steer",
812
+ delivery_disposition: "continuation_pending",
813
+ request_actor: {
814
+ type: "board",
815
+ source: "session",
816
+ userId: "legacy-board-user",
817
+ orgIds: ["00000000-0000-0000-0000-000000000110"],
818
+ isInstanceAdmin: false,
819
+ },
820
+ reconciliation_reason: "legacy_steer_recovered_on_upgrade",
821
+ last_delivery_reason: null,
822
+ },
823
+ {
824
+ position: 2,
825
+ status: "queued",
826
+ delivery_intent: "queue",
827
+ delivery_disposition: null,
828
+ request_actor: {
829
+ type: "agent",
830
+ source: "agent_key",
831
+ orgId: "00000000-0000-0000-0000-000000000110",
832
+ agentId: "00000000-0000-0000-0000-000000000119",
833
+ },
834
+ reconciliation_reason: "legacy_claim_released_on_upgrade",
835
+ last_delivery_reason: "legacy_claim_released_on_upgrade",
836
+ },
837
+ {
838
+ position: 3,
839
+ status: "failed_actionable",
840
+ delivery_intent: "queue",
841
+ delivery_disposition: "failed_actionable",
842
+ request_actor: null,
843
+ reconciliation_reason: "legacy_running_delivery_unconfirmed_on_upgrade",
844
+ last_delivery_reason: "legacy_running_delivery_unconfirmed_on_upgrade",
845
+ },
846
+ {
847
+ position: 4,
848
+ status: "queued",
849
+ delivery_intent: "queue",
850
+ delivery_disposition: null,
851
+ request_actor: null,
852
+ reconciliation_reason: "legacy_request_actor_unavailable",
853
+ last_delivery_reason: "legacy_request_actor_unavailable",
854
+ },
855
+ {
856
+ position: 5,
857
+ status: "queued",
858
+ delivery_intent: "queue",
859
+ delivery_disposition: null,
860
+ request_actor: null,
861
+ reconciliation_reason: "legacy_request_actor_unavailable",
862
+ last_delivery_reason: "legacy_request_actor_unavailable",
863
+ },
864
+ ]);
865
+ const activeIndex = await verifySql.unsafe(`
866
+ SELECT indexname
867
+ FROM pg_indexes
868
+ WHERE schemaname = 'public'
869
+ AND indexname = 'chat_generations_active_conversation_uq'
870
+ `);
871
+ expect(activeIndex).toEqual([{ indexname: "chat_generations_active_conversation_uq" }]);
872
+ }
873
+ finally {
874
+ await verifySql.end();
875
+ }
876
+ }, 20_000);
248
877
  it("rolls back migration 0100 when a legacy alias conflicts with another organization route", async () => {
249
878
  const connectionString = await createTempDatabase();
250
879
  await applyPendingMigrations(connectionString);