diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index de3051b..20e0ce4 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -4,6 +4,12 @@ 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"] @@ -24,3 +30,89 @@ def pytest_configure(config): 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"]