""" Shared pytest fixtures. Integration tests that need a real database should use a separate PostgreSQL instance (e.g. a Docker service spun up by the CI job) and set DATABASE_URL in the environment before importing the app. For unit tests of pure functions no database is needed at all. The fixtures below are the ONE definition of three things that used to be copied into a dozen test modules each (#2825). They are deliberately not autouse: a module opts in with ``pytestmark = pytest.mark.usefixtures("")`` (or a test names the fixture as a parameter), so a unit test that never touches the engine or the MCP context pays nothing for them. """ import os from unittest.mock import AsyncMock, patch import pytest import pytest_asyncio @pytest.fixture(autouse=True) def _isolate_env(request, monkeypatch): """Prevent unit tests from accidentally reading production env vars. Integration tests (marked `integration`) are skipped here: they must use the real DATABASE_URL injected by the CI integration lane, not the fake one. """ if request.node.get_closest_marker("integration"): return monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") monkeypatch.setenv("SECRET_KEY", "test-secret-key") monkeypatch.setenv("OLLAMA_URL", "http://localhost:11434") @pytest.fixture def _bind_user(): """Bind MCP caller #7 for the duration of a test. The MCP tool layer reads the caller from a ContextVar the HTTP transport sets per request; a unit test of a tool has no request, so it binds the caller itself. Every tool-layer test module opts in with ``pytestmark = pytest.mark.usefixtures("_bind_user")`` and builds its fakes with user_id=7 so ownership checks see the caller as the owner. """ from scribe.mcp._context import _user_id_ctx token = _user_id_ctx.set(7) yield _user_id_ctx.reset(token) @pytest_asyncio.fixture async def _dispose_engine(): """Dispose the app's module-level engine after a test that hit Postgres. The engine pools asyncpg connections per event loop, but pytest-asyncio runs each test on a fresh loop — so without this, test 2 gets handed test 1's connection bound to a now-dead loop ("Future attached to a different loop"). Disposing in the test's own loop teardown clears the pool cleanly. The import is deferred so merely collecting a module that mixes unit and integration tests never builds an engine. """ from scribe.models import engine yield await engine.dispose() @pytest.fixture def _no_supersession(): """Stub the auto-inject menu's "which lines are superseded?" lookup (#278). That is a real database call on a path the plugin-context tests exercise without one. Stubbed to "nothing superseded" — the ordinary state — rather than hidden behind a try/except in the product, which would make the code lie about what it does. The label's own behaviour is covered in tests/test_supersession_ranking.py. """ with patch("scribe.services.plugin_context.superseded_ids", AsyncMock(return_value=set())): yield