fix(tests): per-request session via QueuePool + WAL (closes #105) #122
No reviewers
Labels
No labels
agentic-osint
audit
blocked:commit-ready-failed
blocked:gate-fetch
blocked:gate-fetch
blocked:gate-lint
blocked:gate-plan
blocked:gate-research
blocked:needs-decision
blocked:needs-human
blocked:no-changes
blocked:preflight
blocked:verify-failed
bug
chore
deferred
enhancement
feature-request
in-progress
lint-baseline
lint-cleanup
needs-decompose
needs-triage
performance
quickfix
security/medium
triage-approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Mark/osint-hub!122
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/105-per-request-session-v2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Re-submission of #121 (which had a non-mergeable state because the base was behind origin/main). Branch is now based on
abdf87d(origin/main); diff is identical to #121 minus the obsolete base.What
Replace the shared-session override in
tests/conftest.pywith a per-request session strategy backed by QueuePool + WAL SQLite. Concurrent FastAPI requests now get independent SQLAlchemy Sessions with their own DBAPI connections — the actual shape needed under concurrency (SQLAlchemy Session and DBAPI connections are not thread-safe to share).Why
tests/conftest.py:84-92yielded a single Session to every concurrent FastAPI request. Sessions are not thread-safe, so concurrent requests corrupted each other's identity map and transaction state, producing non-deterministicPendingRollbackError(500) and spurious 403Insufficient case accessresponses.The issue body says 13 of 14 concurrency tests fail on pristine main. Since the original report, #96/#101 landed, so the current baseline is 12 of 14. This PR makes all 14 pass.
Changes
backend/tests/fixtures/database.pyStaticPool+ in-memory toQueuePool+ file-backed SQLite withjournal_mode=WAL. WAL allows concurrent readers and writers across multiple connections withoutdatabase table is lockederrors.clean_dbfixture that truncates all tables before each test (FK enforcement is disabled during the truncate). The old "wrap each test in a transaction that gets rolled back at teardown" pattern doesn't compose with per-request connections.db_sessionrebinds to the engine (no longer to a per-test connection), so committed data is visible to per-request sessions through the shared SQLite database.db_session_factoryyields a fresh Session per FastAPI request, with anafter_commithook that removes corresponding ORM objects fromdb_session.identity_mapafter an API delete. The hook preserves the original shared-session behavior expected by tests liketest_delete_case_*_succeeds(which assertdb_session.get(...) is Noneafter an API delete).db_connectionfixture.backend/tests/conftest.pyoverride_get_sessionnow consumesdb_session_factoryand yields a fresh per-request Session.backend/tests/integration/test_integration_concurrent.py_grant_case_accesshelper that grants a user case-level membership viaCaseAccessService.ensure_membership.editor/viewermembership — required after #68 (case-level access control).test_user_cannot_modify_other_users_case: flipped the assertion from 200 to 403 to match the test's stated intent (name says "cannot" but it asserted 200 — pre-#68 stale).Verification
tests/integration/test_integration_concurrent.py: 14/14 pass (10/10 runs, stable).tests/integration/test_api_cases.py: 47/47 pass (3/3 runs, stable).ruff checkandruff format --check: clean on changed files.Risk
Low. The fixture refactor is internal to the test harness — no production code touched. Pre-existing failures in unrelated test modules (e.g.
test_modules_twitter.pyassertingrequires_config == []on a tuple,test_api_errors.py/test_integration_resilience.pymodule-flow assertions) are out of scope for #105 and remain failing.Closes #105 (replaces #121).