Spaces:
Sleeping
Sleeping
| """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"] = "" | |
| # 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 ββββββββββββββββββββββββββββββββββββββββββββ | |
| def event_loop_policy() -> None: | |
| """Use the default asyncio event loop policy.""" | |
| return None | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def reset_vs() -> Generator[None, None, None]: | |
| """Reset the vector store singleton before each test.""" | |
| reset_vectorstore() | |
| yield | |
| reset_vectorstore() | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def mock_embedder() -> MockEmbeddingClient: | |
| """Return the deterministic mock embedding client.""" | |
| return MockEmbeddingClient() | |
| # ββ Sample chunks βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| ] | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 | |