refactor(tests): one definition each for the copied fixtures and fakes (#2825, milestone 296 area 1)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 18s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 18s
The shape ledger showed the same test scaffolding defined over and over: _bind_user x12 (byte-identical), _dispose_engine x10 in three wordings, _no_supersession x3, _make_mock_session x7 in three subsets, a get-or-create User helper x2 (+3 inlined), and fifteen hand-rolled MagicMock note factories each re-explaining the same "an auto-MagicMock attribute is truthy" hazard (note 2109). Now: conftest.py carries _bind_user / _dispose_engine / _no_supersession as opt-in fixtures (pytestmark = usefixtures(...) per module, so unit tests pay nothing), and tests/helpers.py carries make_mock_session(), ensure_user() and fake_note(**attrs) — the hazard documented once, real values on every attribute the product reads. Call sites were rewritten by AST so titles with dashes and commas survived; the three SimpleNamespace _note stand-ins that only feed a single function stay local. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,19 @@ 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("<name>")`` (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)
|
||||
@@ -23,3 +33,52 @@ def _isolate_env(request, monkeypatch):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user