Three flakes in one day, never a captured name — the name finally came from a discriminating run instead of patience: pointing OLLAMA_HOST at a dead port failed exactly eight tests, all in tests/agents/test_tatlock_agent.py, all already marked integration, all running in the unit gate anyway because make test excluded by directory and they live outside the ignored directories. Eight tests doing ~110 seconds of real LLM inference against the production wrapper on every unit run, failing whenever the backend was mid-deploy — which this week it constantly was. The second coupling sat in the session init: conftest ran the real health probes, an HTTP round trip to whatever answers behind OLLAMA_HOST plus a real Anthropic API call whenever the dev .env carries a key, so the cached backend globals followed the network of the moment. The probes are stubbed to the deterministic local-first state; tests needing other states patch the globals themselves, as the selector tests always did. make test now enforces -m "not integration" alongside the directory ignores, and the acceptance is blunt: 679 passed in ~12 s, identical against a dead backend and no API key — down from ~129 s of infrastructure-coupled runtime. Both prongs mutation-checked: the gate removed fails eight against a dead backend; the stub removed fails the new session-globals test. T-8 files the orphaned tests/integration directory that no make target runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""
|
|
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}
|