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>
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""
|
|
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("<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)
|
|
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
|