"""Shared pytest fixtures for all test modules. All LLM and embedding calls are mocked. Isolation tests use a LangChain FAISS index rooted in a per-invocation temporary directory (no shared disk state). """ import os import tempfile import threading from collections.abc import AsyncGenerator, Generator from pathlib import Path from typing import Any import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine # ── Force mock/test settings before importing anything from app ─────────────── os.environ.setdefault("OPENAI_API_KEY", "") os.environ.setdefault("DEV_MODE", "true") # Override developer .env so tests never require a live Qdrant instance. os.environ["VECTORSTORE_BACKEND"] = "faiss" os.environ["ENABLE_ASYNC_PIPELINE"] = "false" os.environ["ENABLE_SPECULATIVE_EXECUTOR"] = "false" os.environ["ENABLE_PROMPT_CACHING"] = "false" os.environ["ENABLE_TEMPORAL_WORKFLOW"] = "false" os.environ["SEMANTIC_CACHE_ENABLED"] = "false" os.environ["ENABLE_JOB_QUEUE"] = "false" os.environ["REDIS_URL"] = "" os.environ["ENABLE_PARALLEL_SECTION_GENERATION"] = "false" os.environ["ALLOW_SQLITE_PARALLEL_SECTIONS"] = "false" # These must be imported after env vars are set from app.db.database import Base, get_db # noqa: E402 from app.embeddings.mock_client import MockEmbeddingClient # noqa: E402 from app.main import create_app # noqa: E402 from app.models.schemas import Chunk # noqa: E402 from app.vectorstore.factory import reset_vectorstore # noqa: E402 # ── In-memory SQLite test database ──────────────────────────────────────────── @pytest.fixture(scope="session") def event_loop_policy() -> None: """Use the default asyncio event loop policy.""" return None @pytest_asyncio.fixture async def test_db() -> AsyncGenerator[AsyncSession, None]: """Provide an isolated in-memory SQLite database session for each test.""" engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) factory = async_sessionmaker(engine, expire_on_commit=False) async with factory() as session: yield session await engine.dispose() # ── Ephemeral FAISS vector store ────────────────────────────────────────────── @pytest.fixture(autouse=True) def reset_vs() -> Generator[None, None, None]: """Reset the vector store singleton before each test.""" reset_vectorstore() yield reset_vectorstore() @pytest.fixture def in_memory_vs(tmp_path: Path) -> Any: """Return a FAISSVectorStore with an empty index under ``tmp_path``.""" from langchain_community.embeddings import FakeEmbeddings from app.vectorstore.faiss_wrapper import FAISSVectorStore embedding = FakeEmbeddings(size=384) vs = FAISSVectorStore.__new__(FAISSVectorStore) vs._embedding = embedding vs._index_path = tmp_path / "faiss_test" vs._index_path.mkdir(parents=True, exist_ok=True) vs._lock = threading.RLock() vs._store = None return vs # ── Mock embedding client ───────────────────────────────────────────────────── @pytest.fixture def mock_embedder() -> MockEmbeddingClient: """Return the deterministic mock embedding client.""" return MockEmbeddingClient() # ── Sample chunks ───────────────────────────────────────────────────────────── @pytest.fixture def sample_chunks_tenant_a() -> list[Chunk]: """FIXTURE — sample chunks belonging to tenant_A.""" return [ Chunk( chunk_id=f"ca-{i}", doc_id="doc-a", tenant_id="tenant_A", text=f"FIXTURE tenant_A document chunk number {i}.", section_type="paragraph", token_count=10, ) for i in range(3) ] @pytest.fixture def sample_chunks_tenant_b() -> list[Chunk]: """FIXTURE — sample chunks belonging to tenant_B.""" return [ Chunk( chunk_id=f"cb-{i}", doc_id="doc-b", tenant_id="tenant_B", text=f"FIXTURE tenant_B document chunk number {i}.", section_type="paragraph", token_count=10, ) for i in range(3) ] # ── Temp directory ──────────────────────────────────────────────────────────── @pytest.fixture def tmp_dir() -> Generator[Path, None, None]: """Provide a temporary directory that is removed after the test.""" with tempfile.TemporaryDirectory() as d: yield Path(d) # ── FastAPI test client ─────────────────────────────────────────────────────── @pytest_asyncio.fixture async def async_client(test_db: AsyncSession) -> AsyncGenerator[AsyncClient, None]: """Provide an async HTTPX client pointed at the test FastAPI app. The database dependency is overridden with the in-memory test session. """ app = create_app() async def _override_db() -> AsyncGenerator[AsyncSession, None]: yield test_db app.dependency_overrides[get_db] = _override_db async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", headers={"X-Tenant-ID": "tenant_test"}, ) as client: yield client