""" 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 @pytest.fixture(autouse=True) def _no_rule_arm(): """Stub the write-path hint's standing-RULES arm (milestone 307). Autouse, and deliberately so. The arm calls semantic_search_rules, which loads the embedding model — so every unrelated plugin-context test that already stubs the NOTES search would otherwise pull a real model into a unit test through the one arm it forgot to stub. The forty-odd existing call sites should not each have to learn about a new arm. The arm's own behaviour is covered where it belongs: the document shape in tests/test_services_rule_embeddings.py, the surfacing rules against real Postgres in tests/test_integration_rule_surfacing.py, and the hook's dedup channel in tests/test_write_path_trigger.py. A test that wants the arm live can re-patch it. """ with patch("scribe.services.plugin_context.semantic_search_rules", AsyncMock(return_value=[])): yield