Files
Chris Coutinho 9a6a253858 fix(tests): Add singleton reset fixture to prevent anyio.WouldBlock errors
Add module-scoped autouse fixture `reset_all_singletons` in
tests/integration/conftest.py that resets all global singletons
between test modules:

- _qdrant_client (vector/qdrant_client.py)
- _embedding_service, _bm25_service (embedding/service.py)
- _provider (providers/registry.py)
- _vector_sync_state with memory streams (app.py)
- _tracer (observability/tracing.py)
- _registry (auth/client_registry.py)
- _token_exchange_service (auth/token_exchange.py)

This fixes anyio.WouldBlock errors that occurred when running the
full integration test suite together. The errors were caused by
stale singleton state holding references to dead event loops or
closed memory streams from previous test modules.

Results:
- Before: 22 passed, 26 errors (WouldBlock), 12 failed
- After: 48 passed, 25 skipped, 1 failed (unrelated timeout)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 09:12:21 -06:00

119 lines
4.4 KiB
Python

"""Pytest configuration for integration tests.
This conftest.py provides hooks and fixtures specific to integration tests,
including the --provider flag for RAG tests.
"""
import logging
import pytest
logger = logging.getLogger(__name__)
# Valid provider names
VALID_PROVIDERS = ["openai", "ollama", "anthropic", "bedrock"]
def pytest_addoption(parser):
"""Add --provider command line option for RAG tests."""
parser.addoption(
"--provider",
action="store",
default=None,
choices=VALID_PROVIDERS,
help="LLM provider for RAG tests: openai, ollama, anthropic, bedrock",
)
def pytest_configure(config):
"""Configure custom markers."""
config.addinivalue_line(
"markers", "rag: mark test as RAG integration test (requires --provider flag)"
)
@pytest.fixture(autouse=True, scope="module")
async def reset_all_singletons():
"""Reset ALL global singletons between test modules.
Prevents anyio.WouldBlock errors caused by stale singleton state
from previous test modules holding references to dead event loops
or closed memory streams.
"""
# Import all modules with singletons
import nextcloud_mcp_server.app as app_module
import nextcloud_mcp_server.auth.client_registry as client_registry_module
import nextcloud_mcp_server.auth.token_exchange as token_exchange_module
import nextcloud_mcp_server.embedding.service as embedding_module
import nextcloud_mcp_server.observability.tracing as tracing_module
import nextcloud_mcp_server.providers.registry as registry_module
import nextcloud_mcp_server.vector.qdrant_client as qdrant_module
# Store originals for restoration after test
originals = {
"qdrant_client": qdrant_module._qdrant_client,
"embedding_service": embedding_module._embedding_service,
"bm25_service": embedding_module._bm25_service,
"provider": registry_module._provider,
"vector_sync_state": (
app_module._vector_sync_state.document_send_stream,
app_module._vector_sync_state.document_receive_stream,
app_module._vector_sync_state.shutdown_event,
app_module._vector_sync_state.scanner_wake_event,
),
"tracer": tracing_module._tracer,
"registry": client_registry_module._registry,
"token_exchange_service": token_exchange_module._token_exchange_service,
}
# Close any open memory streams before reset
if app_module._vector_sync_state.document_send_stream is not None:
try:
await app_module._vector_sync_state.document_send_stream.aclose()
except Exception:
pass
if app_module._vector_sync_state.document_receive_stream is not None:
try:
await app_module._vector_sync_state.document_receive_stream.aclose()
except Exception:
pass
# Reset all singletons to None/fresh state
qdrant_module._qdrant_client = None
embedding_module._embedding_service = None
embedding_module._bm25_service = None
registry_module._provider = None
app_module._vector_sync_state.document_send_stream = None
app_module._vector_sync_state.document_receive_stream = None
app_module._vector_sync_state.shutdown_event = None
app_module._vector_sync_state.scanner_wake_event = None
tracing_module._tracer = None
client_registry_module._registry = None
token_exchange_module._token_exchange_service = None
logger.debug("All singletons reset for test module")
yield
# Cleanup: Close async resources created during test
if qdrant_module._qdrant_client is not None:
try:
await qdrant_module._qdrant_client.close()
except Exception:
pass
# Restore originals
qdrant_module._qdrant_client = originals["qdrant_client"]
embedding_module._embedding_service = originals["embedding_service"]
embedding_module._bm25_service = originals["bm25_service"]
registry_module._provider = originals["provider"]
(
app_module._vector_sync_state.document_send_stream,
app_module._vector_sync_state.document_receive_stream,
app_module._vector_sync_state.shutdown_event,
app_module._vector_sync_state.scanner_wake_event,
) = originals["vector_sync_state"]
tracing_module._tracer = originals["tracer"]
client_registry_module._registry = originals["registry"]
token_exchange_module._token_exchange_service = originals["token_exchange_service"]