CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Failing after 1m16s
CI & Build / Build & push image (push) Skipped
find_duplicate_rule was title-only, on the stated premise that rules are not a semantic-retrieval surface - false since rules were embedded. A preference restating a rule under another title passed untouched, and since both kinds share one ranking, the weaker label could arrive alone. find_overlapping_rules queries semantic_search_rules with the rule_document shape, both kinds, in the scope the new record ranks in (global: every rule the caller owns; project: global + that project). All three MCP create doors call it before creating and return overlaps + overlap_note on the reply. It advises rather than blocks, on measurement: across 16 sampled records the nearest DISTINCT neighbour reached 0.853, while a true rewording scored 0.850. No threshold separates the bands, so the floor (0.80) sits below the restatement and the author judges. The stale docstring is corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
167 lines
6.7 KiB
Python
167 lines
6.7 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_embedding():
|
|
"""Stub the fire-and-forget embedding refresh a note write detaches.
|
|
|
|
For integration tests about ids, transactions and access rather than
|
|
recall: `embed_note` spawns a task that loads the embedding model, which
|
|
outlives the test's event loop and makes the lane slower for nothing. Opt
|
|
in alongside `_dispose_engine`.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
|
|
# Milestones embed too since milestone 415; a plan created in a test would
|
|
# otherwise detach the same model-loading task.
|
|
with patch("scribe.services.notes.embed_note", MagicMock()), \
|
|
patch("scribe.services.milestones.embed_milestone", MagicMock()):
|
|
yield
|
|
|
|
|
|
@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_task_log_arm():
|
|
"""Stub the task-log read arm that get_task / list_tasks / get_milestone
|
|
grew in #4241.
|
|
|
|
Autouse for the reason _no_rule_arm is: those three tools now read work
|
|
logs, and the reads go through the access layer to Postgres. Every unit
|
|
test that opens a task — and most of them do, because a task is what this
|
|
codebase is mostly about — would otherwise try to reach the fake
|
|
DATABASE_URL this file sets, to learn that a fake task has no logs.
|
|
|
|
The arm's own behaviour is covered where it belongs: the payload shape and
|
|
the tool wiring in tests/test_task_work_log_surface.py, which re-patches
|
|
these explicitly, and the ACL scoping against real Postgres in
|
|
tests/test_integration_task_work_log.py. A test that wants the arm live
|
|
re-patches it, same as the rules arm.
|
|
"""
|
|
with patch("scribe.services.task_logs.logs_for_task",
|
|
AsyncMock(return_value=[])), \
|
|
patch("scribe.services.task_logs.count_logs_for_task",
|
|
AsyncMock(return_value=0)), \
|
|
patch("scribe.services.task_logs.log_counts_for_tasks",
|
|
AsyncMock(return_value={})):
|
|
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
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_rule_overlap():
|
|
"""Stub the rule/preference create path's overlap check (#4134).
|
|
|
|
The same reason as _no_rule_arm, one door over: every create_rule /
|
|
create_project_rule / create_preference now asks semantic_search_rules
|
|
whether an existing record answers the same moment, so each existing
|
|
rule-tool unit test would load the embedding model through a call it never
|
|
meant to make. The check's own behaviour is tested in
|
|
tests/test_rule_overlap_gate.py, which binds the real function at import
|
|
time — before this patch runs — and stubs the search beneath it instead.
|
|
"""
|
|
with patch("scribe.services.dedup.find_overlapping_rules",
|
|
AsyncMock(return_value=[])):
|
|
yield
|