Files
tatlock/tests/conftest.py
T
jpmschweitzerandClaude Fable 5 8808880145 fix(tests): T-7 — the unit suite is hermetic, the flake class is gone
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>
2026-09-13 11:29:10 +02:00

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}