Fixes the flaky migration-smoke failure in run #238, on PR #99, which only changes CLAUDE.md:
FAIL apps/api/tests/module-database.integration.ts > … > stops cleanly: …
AssertionError: expected 1 to be +0 (module-database.integration.ts:764)
Cause. After running.shutdown(), four tests immediately count the database's rows in pg_stat_activity and expect zero. PostgreSQL ends a backend asynchronously: pool.end() resolves once the client side has closed, and the server can still list that backend for a moment. On a loaded shared runner, an immediate count sometimes sees it. Nothing actually leaks.
Reproduced. Against postgres:18.4-alpine3.24 limited to 0.05 CPU, the backend was still listed right after pool.end() resolved in 7 of 300 trials, and gone about 1 ms later. With no CPU limit: 0 of 300.
Fix (tests only). A connectionsLeftAfterShutdown() helper waits up to 3 s for the count to reach zero. It's used by all four post-shutdown checks: one in module-database.integration.ts, three in api-http.integration.ts. A real leak is still caught: an idle pooled client stays listed until pg-pool's 10 s idle timeout (pg-pool 3.14's default; Core's pools don't override it), well past the 3 s deadline. No production code changes.
Fixes the flaky `migration-smoke` failure in [run #238](https://git.zayin.net/Zayin/Ekklesia/actions/runs/294), on PR #99, which only changes `CLAUDE.md`:
```
FAIL apps/api/tests/module-database.integration.ts > … > stops cleanly: …
AssertionError: expected 1 to be +0 (module-database.integration.ts:764)
```
**Cause.** After `running.shutdown()`, four tests immediately count the database's rows in `pg_stat_activity` and expect zero. PostgreSQL ends a backend asynchronously: `pool.end()` resolves once the client side has closed, and the server can still list that backend for a moment. On a loaded shared runner, an immediate count sometimes sees it. Nothing actually leaks.
**Reproduced.** Against `postgres:18.4-alpine3.24` limited to 0.05 CPU, the backend was still listed right after `pool.end()` resolved in **7 of 300** trials, and gone about 1 ms later. With no CPU limit: 0 of 300.
**Fix (tests only).** A `connectionsLeftAfterShutdown()` helper waits up to 3 s for the count to reach zero. It's used by all four post-shutdown checks: one in `module-database.integration.ts`, three in `api-http.integration.ts`. A real leak is still caught: an idle pooled client stays listed until pg-pool's 10 s idle timeout (`pg-pool` 3.14's default; Core's pools don't override it), well past the 3 s deadline. No production code changes.
**Verified.** `pnpm test:migrations:integration` passes (401/401, live PostgreSQL). `pnpm verify` passes (2,639 tests).
Related but separate: #97 is the other flaky test seen in this job (the 1 s signed-URL expiry). It's not touched here.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Four integration tests shut the API down and immediately count the
database's rows in pg_stat_activity, expecting zero. PostgreSQL ends a
backend asynchronously: pool.end() resolves once the client side has
closed, and the server can still list that backend for a moment. On a
loaded runner an immediate count saw one, failing migration-smoke on a
docs-only PR (Ekklesia CI run #238, module-database.integration.ts:764,
"expected 1 to be +0").
Reproduced against postgres:18.4-alpine3.24 limited to 0.05 CPU: after
pool.end() resolved, the backend was still listed in 7 of 300 trials,
and gone about a millisecond later.
The checks now wait up to 3 s for the count to reach zero. A connection
that is genuinely left open is still caught: an idle pooled client
stays listed for pg-pool's 10 s idle timeout, far past the deadline.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the flaky
migration-smokefailure in run #238, on PR #99, which only changesCLAUDE.md:Cause. After
running.shutdown(), four tests immediately count the database's rows inpg_stat_activityand expect zero. PostgreSQL ends a backend asynchronously:pool.end()resolves once the client side has closed, and the server can still list that backend for a moment. On a loaded shared runner, an immediate count sometimes sees it. Nothing actually leaks.Reproduced. Against
postgres:18.4-alpine3.24limited to 0.05 CPU, the backend was still listed right afterpool.end()resolved in 7 of 300 trials, and gone about 1 ms later. With no CPU limit: 0 of 300.Fix (tests only). A
connectionsLeftAfterShutdown()helper waits up to 3 s for the count to reach zero. It's used by all four post-shutdown checks: one inmodule-database.integration.ts, three inapi-http.integration.ts. A real leak is still caught: an idle pooled client stays listed until pg-pool's 10 s idle timeout (pg-pool3.14's default; Core's pools don't override it), well past the 3 s deadline. No production code changes.Verified.
pnpm test:migrations:integrationpasses (401/401, live PostgreSQL).pnpm verifypasses (2,639 tests).Related but separate: #97 is the other flaky test seen in this job (the 1 s signed-URL expiry). It's not touched here.
🤖 Generated with Claude Code