"""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"]