| ... | ... | @@ -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 | 2 | import * as NodeChildProcess from "node:child_process"; |
| 3 | 3 | import * as NodeFSP from "node:fs/promises"; |
| 4 | 4 | import * as NodePath from "node:path"; |
| ... | ... | @@ -387,6 +387,48 @@ function insertThread( |
| 387 | 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 | 432 | function seedDatabase( |
| 391 | 433 | dbPath: string, |
| 392 | 434 | workspaceRoots: ReadonlyMap<string, string>, |
| ... | ... | @@ -401,17 +443,7 @@ function seedDatabase( |
| 401 | 443 | const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 }); |
| 402 | 444 | try { |
| 403 | 445 | database.exec("BEGIN IMMEDIATE"); |
| 404 | | for (const table of [ |
| 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 | | ]) { |
| 446 | for (const table of SEEDED_PROJECTION_TABLES) { |
| 415 | 447 | database.exec(`DELETE FROM ${table}`); |
| 416 | 448 | } |
| 417 | 449 | const insertProject = database.prepare( |
| ... | ... | @@ -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 | 632 | seedDatabase(dbPath, workspaceRoots, projects, threads, now); |
| 598 | 633 | |
| 599 | 634 | const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals"); |