What's happening
yarn test / yarn test:run fails deterministically on ~33 of 93 test files, every run, with Error: Hook timed out in Nms (whatever hookTimeout is set to — reproduced at both 10s and 30s) on the beforeAll that does:
beforeAll(async () => {
fastify = await createServer();
await fastify.ready();
...
});
Every subsequent test/hook in the same describe then fails with TypeError: Cannot read properties of undefined (reading 'db') (or 'id'/'close') since fastify was never assigned.
What it's not
- Not Postgres connection-pool exhaustion.
pg_stat_activity shows ~6 connections throughout a run (max_connections=100). Capping the pool (extra: { max: 5, idleTimeoutMillis: 1000 } in data-source.ts, test-only) made no difference — reverted.
- Not a stuck
pg_advisory_lock. pg_locks/pg_stat_activity are completely empty during the hang — the process never even opens a connection to Postgres before it hangs. So the block happens before dataSource.initialize()'s connection attempt, not inside it.
- Not CPU contention from running other work in parallel. Reproduces running the single affected file completely alone (
yarn vitest run src/test/server/routes/opportunity.routes.test.ts), with nothing else competing for CPU/DB.
- Not flaky — identical failure count (33 files, 589 passed / 205 skipped tests) across multiple independent full-suite runs.
What I found (via temporary instrumentation, reverted — not committed)
Instrumenting createServer() in src/server/index.ts line-by-line showed execution reaches await fastifyInstance.register(typeormPlugin) and never proceeds — i.e. the hang is inside typeormPlugin → initDatabase() (src/data/index.ts), before dataSource.initialize()'s actual Postgres connection is ever visible on the server side.
The suspicious part: src/server/plugins/typeorm.ts's onClose hook calls dataSource.destroy() whenever a test's fastify.close() runs:
fastify.addHook("onClose", async () => {
if (dataSource.isInitialized) {
await dataSource.destroy();
}
});
dataSource is a module-level singleton guarded by if (!dataSource.isInitialized) in the plugin's registration path — but because onClose tears it fully down again, any test file with multiple describe blocks, each with its own beforeAll(createServer)/afterAll(fastify.close()) (e.g. src/test/server/routes/opportunity.routes.test.ts, which has 8), forces a full initialize() → destroy() → initialize() → ... cycle on the same DataSource/entity-metadata object, repeated once per describe in that one file. All 8 cycles in that file hang identically, every time.
This smells like a TypeORM decorator-metadata or connection-lifecycle issue that shows up specifically on repeated initialize()/destroy() of the same DataSource instance within one process — not something to guess-fix without a deeper look at what state survives a destroy() and what initialize() re-does with it.
Suggested next steps
- Try reproducing minimally: a tiny script that calls
dataSource.initialize() → dataSource.destroy() → dataSource.initialize() in a loop against the real entity list, timing each call, to confirm/deny that repeated re-initialization on the same instance (rather than the test harness) is the actual trigger.
- Consider whether each test file should get its own fresh
DataSource instance (avoiding destroy()+re-initialize() on a shared singleton) rather than sharing the module-level one across describe blocks.
- Worth checking whether this correlates with running the local
docker compose dev be service (nodemon) concurrently against the same Postgres container while running tests on the host — that wasn't ruled out as a contributing factor, only as the sole cause (the hang reproduced with no visible Postgres-side activity either way).
Impact
Blocks yarn test/yarn test:run and the pre-push husky hook for any commit, not just ones touching affected code — currently the only way to commit at all is --no-verify.
What's happening
yarn test/yarn test:runfails deterministically on ~33 of 93 test files, every run, withError: Hook timed out in Nms(whateverhookTimeoutis set to — reproduced at both 10s and 30s) on thebeforeAllthat does:Every subsequent test/hook in the same
describethen fails withTypeError: Cannot read properties of undefined (reading 'db')(or'id'/'close') sincefastifywas never assigned.What it's not
pg_stat_activityshows ~6 connections throughout a run (max_connections=100). Capping the pool (extra: { max: 5, idleTimeoutMillis: 1000 }indata-source.ts, test-only) made no difference — reverted.pg_advisory_lock.pg_locks/pg_stat_activityare completely empty during the hang — the process never even opens a connection to Postgres before it hangs. So the block happens beforedataSource.initialize()'s connection attempt, not inside it.yarn vitest run src/test/server/routes/opportunity.routes.test.ts), with nothing else competing for CPU/DB.What I found (via temporary instrumentation, reverted — not committed)
Instrumenting
createServer()insrc/server/index.tsline-by-line showed execution reachesawait fastifyInstance.register(typeormPlugin)and never proceeds — i.e. the hang is insidetypeormPlugin→initDatabase()(src/data/index.ts), beforedataSource.initialize()'s actual Postgres connection is ever visible on the server side.The suspicious part:
src/server/plugins/typeorm.ts'sonClosehook callsdataSource.destroy()whenever a test'sfastify.close()runs:dataSourceis a module-level singleton guarded byif (!dataSource.isInitialized)in the plugin's registration path — but becauseonClosetears it fully down again, any test file with multipledescribeblocks, each with its ownbeforeAll(createServer)/afterAll(fastify.close())(e.g.src/test/server/routes/opportunity.routes.test.ts, which has 8), forces a fullinitialize()→destroy()→initialize()→ ... cycle on the sameDataSource/entity-metadata object, repeated once perdescribein that one file. All 8 cycles in that file hang identically, every time.This smells like a TypeORM decorator-metadata or connection-lifecycle issue that shows up specifically on repeated
initialize()/destroy()of the sameDataSourceinstance within one process — not something to guess-fix without a deeper look at what state survives adestroy()and whatinitialize()re-does with it.Suggested next steps
dataSource.initialize()→dataSource.destroy()→dataSource.initialize()in a loop against the real entity list, timing each call, to confirm/deny that repeated re-initialization on the same instance (rather than the test harness) is the actual trigger.DataSourceinstance (avoidingdestroy()+re-initialize()on a shared singleton) rather than sharing the module-level one acrossdescribeblocks.docker composedevbeservice (nodemon) concurrently against the same Postgres container while running tests on the host — that wasn't ruled out as a contributing factor, only as the sole cause (the hang reproduced with no visible Postgres-side activity either way).Impact
Blocks
yarn test/yarn test:runand thepre-pushhusky hook for any commit, not just ones touching affected code — currently the only way to commit at all is--no-verify.