diff --git a/packages/core/src/__test-utils__/pg-test-harness.ts b/packages/core/src/__test-utils__/pg-test-harness.ts index 6461c6d6b1..694e727b15 100644 --- a/packages/core/src/__test-utils__/pg-test-harness.ts +++ b/packages/core/src/__test-utils__/pg-test-harness.ts @@ -267,6 +267,157 @@ function adminExecAsync(statement: string, timeoutMs = 15_000): Promise { }); } +/** + * FNXC:PgTestTemplateDb 2026-07-16-17:40: + * Slow-test fix: applying the full Fusion schema baseline on every fresh test + * database cost ~530ms per test file. With ~23 pg-gate files fanned across + * forks against one PostgreSQL server, those DDL applies serialize and dominate + * gate wall-time (mission-store.pg alone measured 6s isolated / 28s under load). + * + * Instead we apply the schema ONCE per worker process into a reusable template + * database, then create each test database with `CREATE DATABASE ... TEMPLATE` + * — a fast server-side file copy that skips re-running the DDL. The template is + * keyed by pid so concurrent forks never collide, and dead-pid templates left + * by crashed/prior runs are swept before creating a new one. + */ +const SCHEMA_TEMPLATE_PREFIX = "fusion_schema_template"; + +/** The per-process template database name (pid-keyed so forks never collide). */ +function templateDbName(): string { + return `${SCHEMA_TEMPLATE_PREFIX}_${process.pid}`; +} + +/** Extract the pid embedded in a template DB name, or null if it doesn't match. */ +function parseTemplatePid(dbName: string): number | null { + const match = new RegExp(`^${SCHEMA_TEMPLATE_PREFIX}_(\\d+)$`).exec(dbName); + if (!match) return null; + const pid = Number.parseInt(match[1], 10); + return Number.isFinite(pid) ? pid : null; +} + +/** + * FNXC:PgTestTemplateDb 2026-07-16-17:40: + * A template left behind by a crashed/finished process is only reclaimable if + * its owning pid is gone. `process.kill(pid, 0)` probes liveness without + * signalling: ESRCH => dead (sweepable); EPERM => alive under another user + * (keep). Treat any non-ESRCH error as alive so we never drop a live template. + */ +function isPidAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code !== "ESRCH"; + } +} + +/** + * Open a short-lived admin connection to the maintenance ("postgres") database + * on the same server, run `fn`, and always close. Used for template lifecycle + * (listing/sweeping/creating template DBs) where we need query results back — + * unlike `adminExecAsync`, which fires psql and returns no rows. + */ +async function withMaintenanceSql( + fn: (client: ReturnType) => Promise, +): Promise { + const maintUrl = new URL(PG_TEST_URL_BASE); + maintUrl.pathname = "/postgres"; + const client = postgres(maintUrl.toString(), { + max: 1, + prepare: false, + onnotice: () => {}, + }); + try { + return await fn(client); + } finally { + await client.end({ timeout: 5 }).catch(() => {}); + } +} + +/** + * FNXC:PgTestTemplateDb 2026-07-16-17:40: + * CREATE DATABASE ... TEMPLATE connects to the source template and errors if + * any other session is attached ("source database is being accessed by other + * users"). Vitest forks get distinct pid-keyed templates, but a single process + * can still issue concurrent `createTaskStoreForTest()` calls, so serialize the + * template-copy step through a chained mutex. + */ +let createFromTemplateChain: Promise = Promise.resolve(); +function serializeTemplateCopy(fn: () => Promise): Promise { + const run = createFromTemplateChain.then(fn, fn); + createFromTemplateChain = run.then( + () => {}, + () => {}, + ); + return run; +} + +/** + * FNXC:PgTestTemplateDb 2026-07-16-17:40: + * Ensure this process's schema-template database exists with the full Fusion + * schema baseline applied, and return its name. Memoized per process so the + * ~530ms baseline apply happens exactly once regardless of how many test files + * or `createTaskStoreForTest()` calls run in the worker. + * + * On entry it sweeps dead-pid templates (crashed prior runs), then recreates + * this pid's template from scratch so a half-built template from an earlier + * aborted run in the same pid can never leak a corrupt schema into copies. + * On failure the memo is cleared so a later call can retry. + */ +let schemaTemplateReady: Promise | null = null; +function ensureSchemaTemplate(): Promise { + if (schemaTemplateReady) return schemaTemplateReady; + const ready = (async (): Promise => { + const templateName = templateDbName(); + await withMaintenanceSql(async (client) => { + // Sweep templates orphaned by crashed/finished processes. + const rows = await client<{ datname: string }[]>` + SELECT datname FROM pg_database + WHERE datname LIKE ${SCHEMA_TEMPLATE_PREFIX + "\\_%"} + `; + for (const row of rows) { + if (row.datname === templateName) continue; + const pid = parseTemplatePid(row.datname); + if (pid !== null && isPidAlive(pid)) continue; + await client + .unsafe(`DROP DATABASE IF EXISTS "${row.datname}" WITH (FORCE)`) + .catch(() => {}); + } + // Recreate this pid's template fresh. + await client + .unsafe(`DROP DATABASE IF EXISTS "${templateName}" WITH (FORCE)`) + .catch(() => {}); + await client.unsafe(`CREATE DATABASE "${templateName}"`); + }); + + // Apply the schema baseline once into the template, then close so the + // template has no live connections when copies are taken from it. + const templateUrl = `${PG_TEST_URL_BASE}/${templateName}`; + const schemaBackend: ResolvedBackend = { + mode: "external", + runtimeUrl: templateUrl, + migrationUrl: templateUrl, + migrationUrlOverridden: false, + }; + const schemaConnections = await createConnectionSetFromUrl(schemaBackend, { + poolMax: 1, + connectTimeoutSeconds: 5, + }); + try { + await applySchemaBaseline(schemaConnections.migration); + } finally { + await schemaConnections.close(); + } + return templateName; + })(); + ready.catch(() => { + // Allow a later call to rebuild the template after a transient failure. + if (schemaTemplateReady === ready) schemaTemplateReady = null; + }); + schemaTemplateReady = ready; + return ready; +} + /** * FNXC:TestMigrationTail 2026-06-24-16:00: * Create a fresh, isolated PostgreSQL database with the Fusion schema applied, @@ -287,29 +438,32 @@ export async function createTaskStoreForTest(options?: { const prefix = options?.prefix ?? "fusion_test"; const dbName = uniqueDbName(prefix); - try { - await adminExecAsync(`DROP DATABASE IF EXISTS "${dbName}"`); - } catch { - // may not exist — safe to ignore - } - await adminExecAsync(`CREATE DATABASE "${dbName}"`); + + // FNXC:PgTestTemplateDb 2026-07-16-17:40: + // Create the test database as a copy of the pre-baked schema template + // (fast server-side file copy) instead of re-running the schema baseline per + // test. The template apply is memoized once per worker process; the copy is + // serialized because CREATE DATABASE ... TEMPLATE forbids concurrent access + // to the source template. + const template = await ensureSchemaTemplate(); + await serializeTemplateCopy(async () => { + try { + await adminExecAsync(`DROP DATABASE IF EXISTS "${dbName}"`); + } catch { + // may not exist — safe to ignore + } + await adminExecAsync(`CREATE DATABASE "${dbName}" TEMPLATE "${template}"`); + }); const testUrl = `${PG_TEST_URL_BASE}/${dbName}`; - // Apply schema baseline via a dedicated migration connection. + // The database already carries the full schema (copied from the template), + // so open the runtime connection pool and construct the AsyncDataLayer. const schemaBackend: ResolvedBackend = { mode: "external", runtimeUrl: testUrl, migrationUrl: testUrl, migrationUrlOverridden: false, }; - const schemaConnections = await createConnectionSetFromUrl(schemaBackend, { - poolMax: 1, - connectTimeoutSeconds: 5, - }); - await applySchemaBaseline(schemaConnections.migration); - await schemaConnections.close(); - - // Open the runtime connection pool and construct the AsyncDataLayer. const connections = await createConnectionSetFromUrl(schemaBackend, { poolMax, connectTimeoutSeconds: 5,