"""Pytest config — no DB / no network. Tests target pure functions only. Heavy runtime deps (fastapi, httpx, sqlalchemy, pydantic-settings, tenacity) are installed inside the container but not necessarily on the host. Tests that need them use pytest.importorskip; the full suite runs via `docker compose run --rm app pytest tests/`.""" from __future__ import annotations import os import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) # Sentinel env so importing app.config doesn't try to read a missing .env. os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///:memory:") os.environ.setdefault("CASSANDRA_MOCK", "1") import pytest @pytest.fixture(autouse=True) def stub_reviewer(monkeypatch): """Replace review_read with a clean-passing stub in every consumer module. Tests that mock the generator's call_llm shouldn't also have to mock the reviewer that runs after it — the reviewer is a safety gate, not behaviour under test. Tests in test_output_review.py exercise review_read through its own module and are unaffected. Tests that want to assert the reviewer-rejected branch can override with their own monkeypatch.setattr — later wins. """ from app.services.output_review import Verdict async def _clean(_client, _candidate, **_kw): return Verdict(clean=True, reason="stubbed-by-conftest", cost_usd=0.0) for mod_path in ( "app.services.portfolio_analysis", "app.routers.chat", "app.jobs.ai_log_job", "app.jobs.email_digest_job", "app.jobs.indicator_summary_job", ): try: mod = __import__(mod_path, fromlist=["review_read"]) except ImportError: continue if hasattr(mod, "review_read"): monkeypatch.setattr(mod, "review_read", _clean) @pytest.fixture async def db_factory(tmp_path): """Per-test sqlite engine + async session factory. Creates a fresh sqlite database file under ``tmp_path``, applies ``Base.metadata.create_all``, and rebinds ``app.db._engine`` / ``app.db._session_factory`` so module-level helpers (which look these up at call time) see the test engine. Yields the ``async_sessionmaker``. Tests use it like: async def test_foo(db_factory): async with db_factory() as session: ... """ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app import db as db_mod from app.db import Base import app.models # noqa: F401 — registers models on Base.metadata engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/test.db") factory = async_sessionmaker(engine, expire_on_commit=False) db_mod._engine = engine db_mod._session_factory = factory async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield factory await engine.dispose()