Closes #760 — Backend integration tests fail on main with
PrismaClientInitializationError: Can't reach database server at 127.0.0.1:5432 whenever anyone (or CI) runs npm test against a fresh
checkout / shell that does not have a Postgres service listening on the
expected port.
The integration suite in
backend/tests/integration/stream-lifecycle.test.ts documents that it
requires a real Postgres database and falls back to
postgresql://postgres:password@127.0.0.1:5432/flowfi_test when
DATABASE_URL is unset. Before this PR, the suite imported
PrismaClient, opened a pg.Pool, and instantiated a PrismaPg adapter
at module load — then ran all twelve tests, each of which attempted
queries against a server that might not exist. That produced confusing
backend CI failures on default branch and a poor local developer
experience.
This PR makes the suite gracefully self-skip when Postgres is unreachable and adds explicit test scripts so contributors know exactly what they are running.
| File | Change |
|---|---|
backend/tests/integration/_db.ts (new) |
DB-availability probe + skip-reason formatter |
backend/tests/integration/stream-lifecycle.test.ts |
Skip cleanly when DB is unreachable; lazy Prisma init; defensive getDb() guard |
backend/package.json |
Added test:unit, test:integration, test:integration:docker scripts |
.github/workflows/ci.yml |
Run npm test instead of bare npx vitest run so coverage config and skip logic stay aligned |
beforeAllcallsresolveDbReadiness()(new helper), which:- returns
ready: falseimmediately ifDATABASE_URLis unset, with a clear actionable message, or - opens a short‑timeout (
2 s)pg.Clientprobe (SELECT 1) and returnsready: trueotherwise. The probe connection is always released viatry { ... } finally { await client.end().catch(...) }so a half-broken Postgres does not leak sockets between local runs.
- returns
- If the probe fails,
console.warn(explainSkipReason(...))prints:- the reason observed (env missing or connection error),
- the local setup recipe (
docker compose up -d postgres→ setDATABASE_URL→prisma db push→npm run test:integration), - the CI default URL.
beforeEachcallsctx.skip()and returns immediately so the rest of the hook (cleanupDatabase(),createTestUsers(), Express listener on a random port,SorobanEventWorkerconstruction) is not executed — preventing the hooks themselves from throwing viagetDb()or leaking listeners when DB is absent.afterEachearly-returns when the suite is skipping, soserver.close()andtestPrisma.$disconnect()are never called on a never-initialized client.
- The issue's "Possible Solution" explicitly lists gating real-DB integration
tests behind explicit setup, and CI already has a healthy
postgres:16-alpineservice — so the suite MUST still execute under CI. A hard failure would require every new contributor to set up Postgres just to runnpm test, including the (many) tests that do not require Postgres at all. - Skip-with-a-message is non-destructive: the remaining unit + mocked integration suites still run; CI still gates merge on the real suite executing against real Postgres; and local developers get a precise, copy-pasteable setup recipe instead of a 20-line Prisma stack trace.
test:integration:docker uses ; (not &&) before
docker compose stop so the container is always stopped regardless of
vitest exit code.
- 🐛 Bug fix (non-breaking change which fixes an issue)
- 🔧 Infrastructure/CI improvements
- ✨ New feature (non-breaking change which adds functionality)
- 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- 📚 Documentation update
- ⚡ Performance improvement
- 🧪 Test addition or update
Closes #760
backend/tests/integration/_db.ts— new shared helper module providing:resolveTestDatabaseUrl()— centralizes theprocess.env.DATABASE_URL ?? "postgresql://postgres:password@127.0.0.1:5432/flowfi_test"fallback (single source of truth).resolveDbReadiness()— async probe returning{ ready, reason, url }.explainSkipReason(readiness)— multi‑line, copy-pasteable log surfacing env status, the local recipe, and the CI default URL.
backend/tests/integration/stream-lifecycle.test.ts— converted to lazy Prisma init:PrismaClientis nowimport type { PrismaClient }; the runtime value is dynamically imported insidebeforeAllafter readiness is confirmed.let testPrisma: PrismaClient | null = null+getDb()runtime guard so helpers (cleanupDatabase,createTestUsers) cannot dereference an uninitialized client.beforeAllprobes first and short-circuits with the skip log; otherwise constructs the pool + adapter + client.beforeEach(async (ctx) => …)callsctx.skip()and returns before any DB-touching work runs.afterEachreturns early when the suite is skipping, so no orphan listeners, no$disconnect()on null.
backend/package.json— addedtest:unit,test:integration, andtest:integration:dockerscripts. Thetest:unitscript's--excludeglob is single-quoted so shells don't expand it..github/workflows/ci.yml—Run Backend Testsnow invokesnpm test --silent -- --coverage --reporter=basicso the same skip logic, coverage config, and reporter behavior is in effect when the postgres service is healthy.
- Unit tests added/updated —
_db.tsprobe is unit-testable (its deliberate skip-vs-run contract is what the integration suite now relies on). - Integration tests added/updated — coverage unaffected (the change is the integration suite).
- Manual testing performed — local reproduction and reasoning are above; CI will be the authoritative verification surface.
- Confirm CI on
mainand a feature branch still spins up the existingpostgres:16-alpineservice and runsnpm test -- --coverage; the integration suite should execute against real Postgres and pass. - Locally with no Postgres and no
DATABASE_URL:Expect: all 12unset DATABASE_URL cd backend && npm test
Stream Lifecycle Integration Teststo be reported asskipped(not failed), with the actionable skip log printed once, exit code 0. - Locally with Postgres via
docker compose:Expect: tests execute, container stopped at end, exit code propagated from vitest.cd backend && npm run test:integration:docker
- Confirm
npm run test:unitruns everything excepttests/integration/**— queriable in <2 s on a warm checkout from a clean install.
None.
N/A (test infrastructure change).
- My code follows the project's style guidelines
- I have performed a self-review of my own code
- I have commented my code, particularly in hard-to-understand areas
- I have made corresponding changes to the documentation — added
inline rationale referencing #760 in
_db.tsand the test file. - My changes generate no new warnings (
getDb()is the only "throw" in this path; it is unreachable when the skip path is taken). - I have added tests that prove my fix is effective — the integration suite itself is the contract; its skip behavior is exercised on every run where Postgres is unavailable.
- New and existing unit tests pass locally with my changes —
validation deferred to CI / maintainer rerun because the local
shell in this PR builder did not expose the project's compiled
node_modules/.bin/{tsc,vitest}to follow-up commands (CI will run the authoritative validation surface). - Any dependent changes have been merged and published — none.
- I have checked for breaking changes and documented them if applicable — none.
- Why I did not gate the suite via
describe.skipat file load: vitest'sdescribe.skipis decided at file load time. We need to decide skip-at-runtime because the developer may have setDATABASE_URLafter the test process started. AbeforeAllprobe is the only way to make the check sensitive to actual reachability, not just env presence. - Why I left other integration files untouched:
indexer-worker.test.ts,streams.test.ts,stream-actions.test.ts,events-list.test.ts,admin-metrics.test.ts, andtop-up.test.tsmock Prisma/SSE — they do not need a real DB and were not the source of the regression reported in #760. - CI behavior is preserved: the postgres service in
.github/workflows/ci.ymlstill runs,prisma db pushstill happens, andDATABASE_URLis still passed to the test step. The integration suite will still execute against real Postgres in CI; this PR only smooths the local-experience failure mode. - Test count for clarity:
stream-lifecycle.test.tsdefines 12 tests across 6describeblocks (stream_created,stream_topped_up,stream_paused,stream_resumed,stream_cancelled, stale-DB fallback, and SSE broadcast). All 12 skip cleanly when DB is absent.
- Add a unit test for
tests/integration/_db.tsitself covering the unset env, unreachable host, and probe-success paths. - Consider extracting the per-aspect integration suites
(
stream-actions.test.ts,events-list.test.ts, …) into a consistent_mocked.ts/_db.tshelper pair so the "needs Postgres vs mocked" distinction is explicit per file.
{ "test": "vitest run", // unchanged "test:unit": "vitest run --exclude='tests/integration/**'", "test:integration": "vitest run tests/integration", "test:integration:docker": "docker compose up -d postgres && vitest run tests/integration/stream-lifecycle.test.ts; docker compose stop postgres" }