diff --git a/apps/server/test/integration/db.ts b/apps/server/test/integration/db.ts index 29033953..084d47e2 100644 --- a/apps/server/test/integration/db.ts +++ b/apps/server/test/integration/db.ts @@ -38,6 +38,24 @@ export const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL ?? 'postgresql://docmost:docmost_dev_pw@localhost:5432/docmost_test'; +// Build the raw postgres.js client (mirrors database.module.ts: max pool, +// silenced notices, bigint-as-number parsing). Kept separate so the singleton +// can hold a reference to bound its shutdown in destroyTestDb. +function buildTestSql(url: string = TEST_DATABASE_URL) { + return postgres(url, { + max: 5, + onnotice: () => {}, + types: { + bigint: { + to: 20, + from: [20, 1700], + serialize: (value: number) => value.toString(), + parse: (value: string) => Number.parseInt(value), + }, + }, + }); +} + /** * Build a Kysely instance that MIRRORS the app's setup in database.module.ts: * PostgresJSDialect over postgres(), CamelCasePlugin, and the bigint type @@ -47,38 +65,40 @@ export const TEST_DATABASE_URL = */ export function buildTestDb(url: string = TEST_DATABASE_URL): Kysely { return new Kysely({ - dialect: new PostgresJSDialect({ - postgres: postgres(url, { - max: 5, - onnotice: () => {}, - types: { - bigint: { - to: 20, - from: [20, 1700], - serialize: (value: number) => value.toString(), - parse: (value: string) => Number.parseInt(value), - }, - }, - }), - }), + dialect: new PostgresJSDialect({ postgres: buildTestSql(url) }), plugins: [new CamelCasePlugin()], }); } let singleton: Kysely | undefined; +let singletonSql: ReturnType | undefined; /** Lazily-built shared Kysely for the test suite (one per worker; maxWorkers=1). */ export function getTestDb(): Kysely { if (!singleton) { - singleton = buildTestDb(); + singletonSql = buildTestSql(); + singleton = new Kysely({ + dialect: new PostgresJSDialect({ postgres: singletonSql }), + plugins: [new CamelCasePlugin()], + }); } return singleton; } export async function destroyTestDb(): Promise { - if (singleton) { - await singleton.destroy(); - singleton = undefined; + if (!singleton) return; + const sql = singletonSql; + // Clear the refs first so a hung end() cannot leave a half-closed singleton. + singleton = undefined; + singletonSql = undefined; + // postgres.js .end() waits indefinitely for in-flight queries by default; a + // leaked/stuck pooled connection would hang the afterAll hook (a 60s hook + // timeout in CI). Bound the shutdown: the { timeout } grace period lets + // active queries drain, then force-closes lingering sockets so teardown + // always completes. We close the pool directly instead of Kysely.destroy() + // (which would call sql.end() again with no timeout). + if (sql) { + await sql.end({ timeout: 5 }); } } diff --git a/apps/server/test/jest-integration.json b/apps/server/test/jest-integration.json index 30f62397..43f366f5 100644 --- a/apps/server/test/jest-integration.json +++ b/apps/server/test/jest-integration.json @@ -16,6 +16,7 @@ "testEnvironment": "node", "testTimeout": 60000, "maxWorkers": 1, + "forceExit": true, "globalSetup": "/test/integration/global-setup.ts", "globalTeardown": "/test/integration/global-teardown.ts", "moduleNameMapper": {