Parent: #14
Reproduced evidence
After #22's DuckDB isolation fix, diagnostic PR #23 progressed through roughly 19% of 817 backend tests before reaching the next independent hang.
Observed sequence:
tests/test_content_favorite_api.py::test_content_favorite_toggle_returns_favorite_id_and_state PASSED.
- The next visible test was
tests/test_content_pipeline.py::test_update_source_error_uses_readable_fallback_for_blank_message.
- After 30 seconds without progress, faulthandler showed pytest waiting in async-generator fixture setup (
pytest_asyncio / event-loop select), not inside the next test body.
- The 180-second diagnostic timeout ended the run with exit 124.
- PostgreSQL logged multiple client connections terminating with open transactions when the test process was killed.
Root-cause chain confirmed by code tracing
The favorite test fixture itself uses an in-memory SQLite engine, but POST /contents/{content_id}/favorite calls trigger_vector_rebuild(current_user.id) after the favorite write succeeds.
trigger_vector_rebuild():
- calls
asyncio.get_running_loop().create_task(...) without returning/tracking the task;
- opens its own session from the module-level
app.core.database.async_session (the CI PostgreSQL database), not the test fixture's SQLite session;
- runs
rebuild_user_vector() and commits asynchronously.
Therefore the unit-style favorite test can pass while a real PostgreSQL background transaction continues after the response/fixture body. The next global autouse clean_tables fixture then executes TRUNCATE ... CASCADE and can wait behind that task/transaction before the next test body starts.
Objective
Make the content-favorite test hermetic: authorization/API behavior should be tested without launching the production global interest-vector background task.
Minimal test fix
- inject
monkeypatch into contents_client;
- monkeypatch
app.services.interest_vector_service.trigger_vector_rebuild to a no-op for this fixture;
- preserve all favorite toggle/state assertions;
- do not alter production vector behavior in this slice.
Separate runtime concern
The production fire-and-forget task has no lifecycle handle/cancellation/join contract. That is a broader runtime reliability concern and must be tracked separately under #6 rather than hidden inside this test-isolation fix.
Acceptance gates
- targeted order
test_content_favorite_toggle_returns_favorite_id_and_state → test_update_source_error_uses_readable_fallback_for_blank_message completes without fixture hang;
- favorite API assertions remain unchanged;
- favorite unit test does not open a global PostgreSQL interest-vector task/session;
- bounded diagnostic suite progresses beyond this point;
- final PR contains durable test code only, no process artifacts.
Verifier focus
Verify this is proper unit-test isolation rather than disabling production functionality. Production background-task lifecycle remains a separate issue.
Parent: #14
Reproduced evidence
After #22's DuckDB isolation fix, diagnostic PR #23 progressed through roughly 19% of 817 backend tests before reaching the next independent hang.
Observed sequence:
tests/test_content_favorite_api.py::test_content_favorite_toggle_returns_favorite_id_and_statePASSED.tests/test_content_pipeline.py::test_update_source_error_uses_readable_fallback_for_blank_message.pytest_asyncio/ event-loop select), not inside the next test body.Root-cause chain confirmed by code tracing
The favorite test fixture itself uses an in-memory SQLite engine, but
POST /contents/{content_id}/favoritecallstrigger_vector_rebuild(current_user.id)after the favorite write succeeds.trigger_vector_rebuild():asyncio.get_running_loop().create_task(...)without returning/tracking the task;app.core.database.async_session(the CI PostgreSQL database), not the test fixture's SQLite session;rebuild_user_vector()and commits asynchronously.Therefore the unit-style favorite test can pass while a real PostgreSQL background transaction continues after the response/fixture body. The next global autouse
clean_tablesfixture then executesTRUNCATE ... CASCADEand can wait behind that task/transaction before the next test body starts.Objective
Make the content-favorite test hermetic: authorization/API behavior should be tested without launching the production global interest-vector background task.
Minimal test fix
monkeypatchintocontents_client;app.services.interest_vector_service.trigger_vector_rebuildto a no-op for this fixture;Separate runtime concern
The production fire-and-forget task has no lifecycle handle/cancellation/join contract. That is a broader runtime reliability concern and must be tracked separately under #6 rather than hidden inside this test-isolation fix.
Acceptance gates
test_content_favorite_toggle_returns_favorite_id_and_state→test_update_source_error_uses_readable_fallback_for_blank_messagecompletes without fixture hang;Verifier focus
Verify this is proper unit-test isolation rather than disabling production functionality. Production background-task lifecycle remains a separate issue.