diff --git a/eslint.config.mjs b/eslint.config.mjs index c5dd669..fdbdf3f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -19,6 +19,10 @@ const eslintConfig = defineConfig([ "out/**", "build/**", "next-env.d.ts", + // Stryker leaves sandbox copies of the whole tree behind. Linting those + // repeats every finding once per sandbox and fails the run on code that + // is not ours to fix. + ".stryker-tmp/**", ]), ]); diff --git a/tests/global-setup.ts b/tests/global-setup.ts index d779515..c4d428e 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -1,10 +1,57 @@ 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").start(); - process.env.DATABASE_URL = container.getConnectionUri(); + 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(); + } } export async function teardown() { diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index 8b3483b..10f1c7f 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -5,11 +5,35 @@ 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"."