""" Shared test fixtures for all tests. Following FastAPI testing best practices. """ import asyncio import pytest from fastapi.testclient import TestClient from httpx import ASGITransport, AsyncClient from src.main import app @pytest.fixture(scope="session", autouse=True) def _tenant_guard(): """ Hard-fail the whole suite if the effective tenant resolves to the production tenant ("jpmschweitzer"). Isolation is tenant-based: tests that touch shared services (Qdrant memories collections, Wiki.js via library-desk, Neo4j, Redis) must run under the reserved test tenant "llm_tester" (or a test_-prefixed namespace). This mirrors the guard library-desk applies on its side. """ from src.core.config import PRODUCTION_TENANT, config from src.core.context import get_default_user from src.core.multi_tenancy import get_memory_collection_name effective = get_default_user() if effective == PRODUCTION_TENANT or config.effective_default_user == PRODUCTION_TENANT: pytest.exit( f"TENANT GUARD: refusing to run the test suite - the effective " f"tenant resolves to the production tenant '{PRODUCTION_TENANT}' " f"(ENVIRONMENT={config.ENVIRONMENT.value}, " f"DEFAULT_USER={config.DEFAULT_USER}). Tests must run under " f"'llm_tester' or a test_-prefixed tenant.", returncode=1, ) # The Qdrant memories namespace derived from the effective tenant # must never be the production collection. assert get_memory_collection_name(effective) != get_memory_collection_name( PRODUCTION_TENANT ), "test suite would target the production memories collection" @pytest.fixture(scope="session", autouse=True) def _initialize_app(_tenant_guard): """ Run application lifespan (household registration, backend globals) once per test session. ASGITransport doesn't trigger lifespan events, so we call it explicitly. The backend health probes are stubbed (T-7): `make test` promises "no external services", but the real probes coupled every unit run to live infrastructure — an HTTP round trip to whatever answers behind OLLAMA_HOST, and a real Anthropic API call whenever the dev .env carries a key. The suite flaked whenever the local backend was mid-deploy: the ollama probe failed, the cached globals flipped, and any test that consults them unpatched changed behavior. The stubs land the globals in the deterministic local-first state a healthy startup produces; tests needing other states patch the globals themselves, and `make test-integration` runs real probes. Depends on _tenant_guard so the suite refuses to start under the production tenant before any initialization happens. """ from unittest.mock import patch from src.anthropic import model_selector from src.core import startup async def _healthy_local_backend() -> bool: model_selector._ollama_available = True model_selector._local_flavor = "boilerroom" return True async def _no_cloud_fallback() -> bool: model_selector._claude_available = False return False # Patch the names startup actually calls (imported into its module). with ( patch.object(startup, "check_ollama_health", _healthy_local_backend), patch.object(startup, "check_claude_health", _no_cloud_fallback), ): asyncio.run(startup.initialize_application()) @pytest.fixture def client() -> TestClient: """ Synchronous test client for FastAPI. Use for simple tests that don't require async. """ return TestClient(app) @pytest.fixture async def async_client() -> AsyncClient: """ Async test client for FastAPI. Use for testing async endpoints and streaming. """ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: yield client @pytest.fixture def mock_chat_request() -> dict: """Standard chat completion request fixture.""" return { "model": "lorem-tester", "messages": [{"role": "user", "content": "Hello, world!"}], "temperature": 0.7, "stream": False, } @pytest.fixture def mock_streaming_chat_request(mock_chat_request) -> dict: """Streaming chat completion request fixture.""" return {**mock_chat_request, "stream": True}