From 05ec9feaf944f5a9a9996b802cd3a6c8d72dd3e1 Mon Sep 17 00:00:00 2001 From: agent_vscode Date: Mon, 6 Jul 2026 01:28:31 +0300 Subject: [PATCH] =?UTF-8?q?test(int):=20unhang=20test:int=20=E2=80=94=20fo?= =?UTF-8?q?rceExit=20+=20bounded=20destroyTestDb=20(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once the ESM transform fix (8e125799) let the four previously-unparseable int-specs run, the server integration suite stopped exiting: all 66 tests pass, then jest prints "Jest did not exit one second after the test run has completed." and idles until the 20-minute job timeout kills it. Separately, ai-chat-stream.int-spec.ts failed because its afterAll (destroyTestDb) hit the 60s hook timeout — postgres.js .end() waits for in-flight queries forever, so a leaked/stuck pooled connection hung teardown. Pragmatic unblock (the underlying open-handle leak is tracked in #382): - jest-integration.json: add forceExit so jest always exits after the run even if a suite leaves an open handle. - db.ts: capture the singleton's raw postgres sql instance and bound the pool shutdown with sql.end({ timeout: 5 }) (the same bounded-end pattern already used in global-setup.ts) instead of Kysely.destroy(), so destroyTestDb can no longer hang the afterAll hook. forceExit masks residual handle leaks rather than fixing them; the proper investigation (run with --detectOpenHandles on a pg+redis stand, close the leaking timers/sockets, then drop forceExit) is filed as #382. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/server/test/integration/db.ts | 56 +++++++++++++++++--------- apps/server/test/jest-integration.json | 1 + 2 files changed, 39 insertions(+), 18 deletions(-) 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": {