| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off globalDate:off - This host-side fixture creates an isolated local T3 environment. | 1 | // @effect-diagnostics nodeBuiltinImport:off globalTimers:off globalDate:off - This host-side fixture creates an isolated local T3 environment. |
| 2 | import * as NodeChildProcess from "node:child_process"; | 2 | import * as NodeChildProcess from "node:child_process"; |
| 3 | import * as NodeFSP from "node:fs/promises"; | 3 | import * as NodeFSP from "node:fs/promises"; |
| 4 | import * as NodePath from "node:path"; | 4 | import * as NodePath from "node:path"; |
| ... | @@ -387,6 +387,48 @@ function insertThread( | ... | @@ -387,6 +387,48 @@ function insertThread( |
| 387 | .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt); | 387 | .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt); |
| 388 | } | 388 | } |
| 389 | | 389 | |
| | 390 | const SEEDED_PROJECTION_TABLES = [ |
| | 391 | "projection_pending_approvals", |
| | 392 | "projection_thread_proposed_plans", |
| | 393 | "projection_thread_activities", |
| | 394 | "projection_thread_messages", |
| | 395 | "projection_thread_sessions", |
| | 396 | "projection_turns", |
| | 397 | "projection_threads", |
| | 398 | "projection_projects", |
| | 399 | "projection_state", |
| | 400 | ] as const; |
| | 401 | |
| | 402 | function hasSeedableSchema(dbPath: string): boolean { |
| | 403 | let database: NodeSqlite.DatabaseSync; |
| | 404 | try { |
| | 405 | database = new NodeSqlite.DatabaseSync(dbPath, { readOnly: true }); |
| | 406 | } catch { |
| | 407 | return false; |
| | 408 | } |
| | 409 | try { |
| | 410 | const row = database |
| | 411 | .prepare( |
| | 412 | `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`, |
| | 413 | ) |
| | 414 | .get(...SEEDED_PROJECTION_TABLES) as { count: number }; |
| | 415 | return row.count === SEEDED_PROJECTION_TABLES.length; |
| | 416 | } catch { |
| | 417 | return false; |
| | 418 | } finally { |
| | 419 | database.close(); |
| | 420 | } |
| | 421 | } |
| | 422 | |
| | 423 | async function waitForSeedableSchema(dbPath: string, timeoutMs = 60_000): Promise<void> { |
| | 424 | const deadline = Date.now() + timeoutMs; |
| | 425 | while (Date.now() < deadline) { |
| | 426 | if (hasSeedableSchema(dbPath)) return; |
| | 427 | await new Promise((resolve) => setTimeout(resolve, 250)); |
| | 428 | } |
| | 429 | throw new Error(`The environment server did not migrate ${dbPath} within ${timeoutMs}ms.`); |
| | 430 | } |
| | 431 | |
| 390 | function seedDatabase( | 432 | function seedDatabase( |
| 391 | dbPath: string, | 433 | dbPath: string, |
| 392 | workspaceRoots: ReadonlyMap<string, string>, | 434 | workspaceRoots: ReadonlyMap<string, string>, |
| ... | @@ -401,17 +443,7 @@ function seedDatabase( | ... | @@ -401,17 +443,7 @@ function seedDatabase( |
| 401 | const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 }); | 443 | const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 }); |
| 402 | try { | 444 | try { |
| 403 | database.exec("BEGIN IMMEDIATE"); | 445 | database.exec("BEGIN IMMEDIATE"); |
| 404 | for (const table of [ | 446 | for (const table of SEEDED_PROJECTION_TABLES) { |
| 405 | "projection_pending_approvals", | | |
| 406 | "projection_thread_proposed_plans", | | |
| 407 | "projection_thread_activities", | | |
| 408 | "projection_thread_messages", | | |
| 409 | "projection_thread_sessions", | | |
| 410 | "projection_turns", | | |
| 411 | "projection_threads", | | |
| 412 | "projection_projects", | | |
| 413 | "projection_state", | | |
| 414 | ]) { | | |
| 415 | database.exec(`DELETE FROM ${table}`); | 447 | database.exec(`DELETE FROM ${table}`); |
| 416 | } | 448 | } |
| 417 | const insertProject = database.prepare( | 449 | const insertProject = database.prepare( |
| ... | @@ -594,6 +626,9 @@ export async function seedShowcaseEnvironment(input: { | ... | @@ -594,6 +626,9 @@ export async function seedShowcaseEnvironment(input: { |
| 594 | }); | 626 | }); |
| 595 | }), | 627 | }), |
| 596 | ); | 628 | ); |
| | 629 | // The environment server begins listening before it finishes migrating the |
| | 630 | // database, so wait for the schema before deleting from and reseeding it. |
| | 631 | await waitForSeedableSchema(dbPath); |
| 597 | seedDatabase(dbPath, workspaceRoots, projects, threads, now); | 632 | seedDatabase(dbPath, workspaceRoots, projects, threads, now); |
| 598 | | 633 | |
| 599 | const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals"); | 634 | const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals"); |