RICS / app /tests /conftest.py
StormShadow308's picture
Speed up generation and ingestion; add live report progress on /status.
c893230
Raw
History Blame Contribute Delete
5.95 kB
"""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