ci: run backend tests against Postgres, not only in-memory SQLite - #154
Open
lucastononro wants to merge 2 commits into
Open
ci: run backend tests against Postgres, not only in-memory SQLite#154lucastononro wants to merge 2 commits into
lucastononro wants to merge 2 commits into
Conversation
backend-test now spins up a postgres:16-alpine service container and points TEST_DATABASE_URL at it; conftest.py respects TEST_DATABASE_URL (falling back to DATABASE_URL, then the sqlite default for local dev) instead of unconditionally forcing sqlite+aiosqlite. This exercises the Column(JSON) fields, FK ON DELETE CASCADE behavior, and the hand-rolled db._run_migrations against the engine we actually ship. Also pins asyncio_default_fixture_loop_scope/asyncio_default_test_loop_scope to "session" in a new backend/pytest.ini. Without it, pytest-asyncio 1.x's per-test event loop (fine for aiosqlite) breaks the module-level asyncpg engine with "Future attached to a different loop" once a second test needs the DB. Pinning both to session fixes this for both SQLite and Postgres. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
P1 (conftest.py:34): the middle `elif` branch honored an ambient DATABASE_URL from the developer's shell; combined with setup_db's drop_all teardown that could destroy a real dev database on `pytest`. Now only the explicit TEST_DATABASE_URL opt-in overrides; everything else falls back to in-memory SQLite (as before this PR). CI is unaffected — it sets TEST_DATABASE_URL. Adds tests/test_conftest_db_url.py pinning that guarantee. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Owner
Author
Greptile review findings addressed (de197b8)Fixed: 1 / 1 P1 —
|
lucastononro
added a commit
that referenced
this pull request
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
backend/tests/conftest.pyunconditionally forcedDATABASE_URL=sqlite+aiosqlite://, while production runspostgresql+asyncpg. That hid real divergences:models.pyusesColumn(JSON)in ~9 places (SQLite vs Postgres JSON semantics differ), the FKON DELETE CASCADEbehavior, and the hand-rolleddb._run_migrationswere never exercised against the engine we actually ship.backend-testnow runs a realpostgres:16-alpineservice container and points the suite at it.Changes
.github/workflows/ci.yml(backend-test): add apostgres:16-alpineservices:container (health-checked viapg_isready) and a job-levelTEST_DATABASE_URLpointing at it.backend/tests/conftest.py: respectTEST_DATABASE_URL(highest priority — this is what CI sets), thenDATABASE_URLif already set, else fall back to the existing in-memory SQLite default for local dev. No behavior change for anyone not setting either var.backend/pytest.ini(new): pinsasyncio_default_fixture_loop_scope/asyncio_default_test_loop_scopetosession. This was a necessary correctness fix, not just plumbing — without it, pytest-asyncio 1.x's default per-test event loop silently works with aiosqlite but breaks the module-level asyncpg engine withRuntimeError: ... got Future <Future ...> attached to a different loopas soon as a second test needs the DB (asyncpg connections are strictly bound to the loop that created them; the engine is created once at import time). Session-scoping the loop keeps every test on the same loop as the engine, for both SQLite and Postgres.Test plan
Existing
New
postgres:16-alpineviaTEST_DATABASE_URL: 296 passed, 8 skipped — identical pass count to SQLite. The 8 skips are the pre-existingRUN_LLM_E2E-gated live-provider tests intest_providers_e2e.py, unrelated to the database backend; nothing had to be newly skipped or marked to keep Postgres green.Caveats
conftest.pyremains for fast local iteration whenTEST_DATABASE_URL/DATABASE_URLaren't set.Closes #117
🤖 Generated with Claude Code
Greptile Summary
This PR wires the
backend-testCI job to a realpostgres:16-alpineservice container so the test suite exercises the same engine that ships in production, and fixes a previously hidden correctness bug where pytest-asyncio's per-function event loop silently worked with aiosqlite but would raiseRuntimeError: got Future attached to a different loopthe moment asyncpg reused its pooled connections across tests.ci.yml): adds a health-checkedpostgres:16-alpineservice and setsTEST_DATABASE_URLso the job points at it automatically.backend/pytest.ini(new): pinsasyncio_mode = strictand both loop-scope options tosession, which is the correct pytest-asyncio 1.x replacement for the oldevent_loopfixture override.backend/tests/conftest.py: replaces the hard-coded SQLite assignment with an explicitTEST_DATABASE_URL-wins / SQLite-fallback guard, closing the previous concern about ambientDATABASE_URLenv vars pointing at developer databases; adds a regression test to lock this behaviour.Confidence Score: 5/5
Safe to merge — the only finding is a dead pre-existing fixture that pytest-asyncio 1.x silently ignores, causing no runtime harm.
The DATABASE_URL guard in conftest.py is correct and directly addresses the previous review concern; the pytest.ini loop-scope settings are the proper pytest-asyncio 1.x mechanism; the CI Postgres service is health-checked and the credentials are appropriate for an ephemeral test container. The one cleanup item (dead event_loop fixture) is harmless.
backend/tests/conftest.py — the pre-existing event_loop fixture at lines 71-75 is now dead code and can be removed.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[pytest starts] --> B{TEST_DATABASE_URL set?} B -- Yes CI path --> C[os.environ DATABASE_URL = TEST_DATABASE_URL] B -- No local dev path --> D[os.environ DATABASE_URL = sqlite+aiosqlite://] C --> E[SQLAlchemy async engine against Postgres] D --> F[SQLAlchemy async engine against in-memory SQLite] E --> G[pytest.ini: asyncio_default_test_loop_scope = session] F --> G G --> H[setup_db fixture: create_all / drop_all per test] H --> I[Tests run on single session-scoped event loop]%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[pytest starts] --> B{TEST_DATABASE_URL set?} B -- Yes CI path --> C[os.environ DATABASE_URL = TEST_DATABASE_URL] B -- No local dev path --> D[os.environ DATABASE_URL = sqlite+aiosqlite://] C --> E[SQLAlchemy async engine against Postgres] D --> F[SQLAlchemy async engine against in-memory SQLite] E --> G[pytest.ini: asyncio_default_test_loop_scope = session] F --> G G --> H[setup_db fixture: create_all / drop_all per test] H --> I[Tests run on single session-scoped event loop]Reviews (2): Last reviewed commit: "fix(review): address Greptile findings o..." | Re-trigger Greptile