Skip to content

fix(tests): enter TestClient as context manager in test_websocket_state#249

Merged
pcalnon merged 1 commit intomainfrom
fix/canopy-tests-redis-cassandra-ws-metrics-regressions
May 7, 2026
Merged

fix(tests): enter TestClient as context manager in test_websocket_state#249
pcalnon merged 1 commit intomainfrom
fix/canopy-tests-redis-cassandra-ws-metrics-regressions

Conversation

@pcalnon
Copy link
Copy Markdown
Owner

@pcalnon pcalnon commented May 7, 2026

Summary

Fixes the 7 failing tests in src/tests/integration/test_websocket_state.py. The test_client fixture was constructing TestClient(app) and returning it directly, never entering the TestClient as a context manager. FastAPI's lifespan only fires on __enter__, so the module-level backend global in main.py stayed None and every test crashed at main.py:502 with AttributeError: 'NoneType' object has no attribute 'get_status'.

The change is one fixture: wrap in with ... as client: yield client, matching the pattern already used by the project-wide client fixture in src/tests/conftest.py:520.

Investigation context (worth reading — saved 30+ false-positive failures from being chased)

The original bug report listed ~770 errors plus 117 failures on canopy. Triage split them into three categories:

Category Count Root cause Fix
1 ~745 errors + ~85 of the 117 fails ~/.bashrc exports LIBTORCH=/.../rust_mudgeon/.../libtorch and prepends ${LIBTORCH}/lib to LD_LIBRARY_PATH for tch-rs work; the 2023-vintage libtorch_python.so shadows the env's torch and fails with undefined symbol: _PyObject_NextNotImplemented Out-of-repo env hooks at /opt/miniforge3/envs/JuniperCanopy1/etc/conda/{activate,deactivate}.d/ strip rust_mudgeon paths on activate, restore on deactivate. No code change needed.
3 ~30 backend-client + 2 metrics-panel fails When import demo_mode failed (Cat 1), conftest's _reset_all_singletons (wrapped in contextlib.suppress(ImportError)) silently no-op'd, so get_settings.cache_clear() never ran → the Settings lru_cache accumulated demo_mode=True from earlier tests → Redis/Cassandra/MetricsPanel kept short-circuiting through the demo path returning UP+DEMO instead of the configured DISABLED/UNAVAILABLE/DOWN/LIVE No code change — Category 3 dissolved entirely once the env was healthy. The autouse fixture in conftest.py works as designed when the imports succeed.
2 7 fails in test_websocket_state.py This PR: missing with on TestClient → lifespan never runs → backend is None One fixture — this PR.

Test plan

  • All 7 tests in test_websocket_state.py pass after the fix.
  • Full canopy suite run: 2 failures remaining, both pre-existing on origin/main and unrelated to this PR:
    • test_main_endpoints_coverage.py::TestSnapshotDetailRealMode::test_real_mode_snapshot_dir_missing — test/code drift on a 404 detail message ('directory' in 'snapshot not found')
    • test_status_bar_updates.py::TestStatusEndpointFSMIntegration::test_api_status_reflects_training_start — passes in isolation on both branches; flaky in full suite due to test pollution unrelated to this fix
  • Other two files using the same return TestClient(app) anti-pattern (test_phase_b_bridge.py, test_phase_b_pre_b_csrf.py) currently pass because they don't hit lifespan-initialized state — left untouched to keep this change scoped.

Out of scope (follow-up candidates)

  • The two pre-existing failures noted above.
  • _reset_all_singletons swallowing ImportError silently. Removing the suppress would have caught Cat 1 → Cat 3 cascade much earlier, but changing it might destabilize tests that rely on it. Documented in the memory note as future hardening.
  • The other return TestClient(app) callers — currently green, but they're a latent bug if those tests ever start hitting lifespan-initialized globals.

🤖 Generated with Claude Code

…te so lifespan runs

The `test_client` fixture in `test_websocket_state.py` was constructing
`TestClient(app)` and returning it directly, never entering it as a
context manager. FastAPI's lifespan only fires on `__enter__`, so the
module-level `backend` global in `main.py` was never initialized for
these tests. Every test in the file then crashed at the WebSocket
handler's `backend.get_status()` call with:

    AttributeError: 'NoneType' object has no attribute 'get_status'

Fix: wrap the TestClient in `with ... as client: yield client` so
lifespan startup runs (initializing `backend = create_backend(...)`) and
shutdown runs at fixture teardown. This is the same pattern used by the
project-wide `client` fixture in `tests/conftest.py:520`.

Verified: all 7 tests in `test_websocket_state.py` now pass. The other
two test files that use the same anti-pattern (`test_phase_b_bridge.py`
and `test_phase_b_pre_b_csrf.py`) don't hit lifespan-initialized
state and currently pass — left untouched to keep this change scoped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pcalnon pcalnon self-assigned this May 7, 2026
Copy link
Copy Markdown
Owner Author

@pcalnon pcalnon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved

@pcalnon pcalnon merged commit 7657b06 into main May 7, 2026
36 of 53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant