From 464c7ee0e5aad96f3965d83c596954ab6b0be344 Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Sat, 29 Aug 2026 16:14:33 -0700 Subject: [PATCH] fix(tests): revert template-database cloning, restore per-file migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #98 replaced per-file migrations with a template database that each test file cloned via CREATE DATABASE ... TEMPLATE. That broke CI: every integration file now times out in beforeAll at createTestDb(). Test Files 57 failed | 56 passed (113) Error: Hook timed out in 10000ms. 185| beforeAll(async () => { 186| ({ db, close } = await createTestDb()); CREATE DATABASE ... TEMPLATE takes a lock on the source database, so concurrent clones serialize rather than run in parallel. On the CI runner they block past the 10s hook timeout. The change was measured only on a 14-core dev machine, where the suite went 339s -> 21s. That speedup was real but local: the problem it solved — pool connections (14 workers x pg's default 10) overrunning max_connections=100 — never occurred on CI, which has fewer cores and ran the same suite in 27s beforehand. Restore tests/global-setup.ts and tests/integration/setup.ts to their pre-#98 state. The .stryker-tmp ESLint ignore from #98 is unrelated and stays; it takes `pnpm lint` from 1024 errors to 0. The local exhaustion is better addressed with a maxWorkers cap, which costs CI nothing. Not included here — this commit only restores green. --- tests/global-setup.ts | 51 ++------------------------------------ tests/integration/setup.ts | 47 ++++++----------------------------- 2 files changed, 9 insertions(+), 89 deletions(-) diff --git a/tests/global-setup.ts b/tests/global-setup.ts index c4d428eb..d779515b 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -1,57 +1,10 @@ import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql"; -import { drizzle } from "drizzle-orm/node-postgres"; -import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { Pool } from "pg"; -import path from "node:path"; let container: StartedPostgreSqlContainer; -// Every test file clones this database instead of replaying the migration -// chain itself. Shared with tests/integration/setup.ts via TEST_TEMPLATE_DB. -const TEMPLATE_DB = "ledgr_test_template"; - export async function setup() { - container = await new PostgreSqlContainer("postgres:17-alpine") - .withCommand([ - "postgres", - // One throwaway database per test file, all on this single server. The - // default max_connections (100) sits below what the fork pool can demand - // — 14 workers holding a pool each exhausts it, and Postgres starts - // terminating connections mid-run (57P01). - "-c", - "max_connections=300", - // Durability buys nothing for a server destroyed at teardown. - "-c", - "fsync=off", - "-c", - "synchronous_commit=off", - "-c", - "full_page_writes=off", - ]) - .start(); - - const connectionString = container.getConnectionUri(); - process.env.DATABASE_URL = connectionString; - process.env.TEST_TEMPLATE_DB = TEMPLATE_DB; - - // Migrate once, here, into a template database. Cloning that template per - // test file replaces one full migration run per file with a file copy. - const admin = new Pool({ connectionString, max: 1 }); - await admin.query(`CREATE DATABASE "${TEMPLATE_DB}"`); - await admin.end(); - - const templateUrl = new URL(connectionString); - templateUrl.pathname = `/${TEMPLATE_DB}`; - const pool = new Pool({ connectionString: templateUrl.toString(), max: 1 }); - try { - await migrate(drizzle({ client: pool }), { - migrationsFolder: path.join(process.cwd(), "src/db/migrations"), - }); - } finally { - // The template must have no open connections, or CREATE DATABASE ... - // TEMPLATE fails with 55006 for every test file that follows. - await pool.end(); - } + container = await new PostgreSqlContainer("postgres:17-alpine").start(); + process.env.DATABASE_URL = container.getConnectionUri(); } export async function teardown() { diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index 10f1c7f6..8b3483bf 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -5,35 +5,11 @@ import { randomUUID } from "crypto"; import * as schema from "../../src/db/schema"; import path from "node:path"; -// Each worker holds a pool for the life of its test file. Left at pg's default -// of 10 these overrun the server's connection limit once vitest scales forks to -// the core count; the suite needs only a couple of concurrent queries per file. -const POOL_MAX = 4; - -// CREATE DATABASE ... TEMPLATE briefly conflicts when several workers clone the -// same template at once (55006). It clears on its own, so retry rather than -// serialize every worker behind a lock. -const CLONE_RETRIES = 10; - -async function cloneTemplate(admin: Pool, dbName: string, template: string) { - for (let attempt = 0; ; attempt++) { - try { - await admin.query(`CREATE DATABASE "${dbName}" TEMPLATE "${template}"`); - return; - } catch (error) { - const code = (error as { code?: string }).code; - if (code !== "55006" || attempt >= CLONE_RETRIES) throw error; - await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1))); - } - } -} - export async function createTestDb() { const connectionString = process.env.DATABASE_URL || "postgresql://ledgr:ledgr@localhost:5432/ledgr_test"; const dbName = `test_${randomUUID().replace(/-/g, "")}`; - const template = process.env.TEST_TEMPLATE_DB; // Isolate each test file in its own *database* (not a schema). Migrations // reference tables as `"public".""`, which only resolves when the @@ -41,27 +17,18 @@ export async function createTestDb() { // on those qualified references. A throwaway database per file gives every test // its own public schema, keeps the concurrent suite isolated, and is robust to // future migrations regardless of how they qualify identifiers. - const admin = new Pool({ connectionString, max: 1 }); - if (template) { - await cloneTemplate(admin, dbName, template); - } else { - await admin.query(`CREATE DATABASE "${dbName}"`); - } + const admin = new Pool({ connectionString }); + await admin.query(`CREATE DATABASE "${dbName}"`); await admin.end(); const url = new URL(connectionString); url.pathname = `/${dbName}`; - const pool = new Pool({ connectionString: url.toString(), max: POOL_MAX }); + const pool = new Pool({ connectionString: url.toString() }); const db = drizzle({ client: pool, schema }); - - // The template arrives already migrated. Without one (a direct DATABASE_URL, - // no global setup) fall back to replaying the chain. - if (!template) { - await migrate(db, { - migrationsFolder: path.join(process.cwd(), "src/db/migrations"), - }); - } + await migrate(db, { + migrationsFolder: path.join(process.cwd(), "src/db/migrations"), + }); return { db, @@ -69,7 +36,7 @@ export async function createTestDb() { await pool.end(); // DROP DATABASE cannot run while connections are open; FORCE terminates any // stragglers (Postgres 13+). - const admin2 = new Pool({ connectionString, max: 1 }); + const admin2 = new Pool({ connectionString }); await admin2.query(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`); await admin2.end(); },