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

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:
2026-08-21 11:03:48 -04:00
co-authored by Claude Fable 5
parent dad56a51bd
commit bbee0d0db1
38 changed files with 316 additions and 609 deletions
+21 -50
View File
@@ -1,22 +1,10 @@
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from tests.helpers import fake_note
@pytest.fixture(autouse=True)
def _no_supersession():
"""The auto-inject menu now asks which of its lines are superseded (#278).
That is a real database call on a path these 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
pytestmark = pytest.mark.usefixtures("_no_supersession")
def _rule(rid, title, topic_id):
@@ -26,21 +14,6 @@ def _rule(rid, title, topic_id):
return r
def _note(nid, title, user_id=1, note_type="note", is_task=False, task_kind="work"):
n = MagicMock()
n.id, n.title = nid, title
# Real values, defaulting to the caller used in these tests: the injected menu
# compares user_id to decide whether the line needs a "shared by …"
# attribution, and reads is_task/task_kind/note_type for the kind marker. An
# auto-MagicMock is truthy, so every line would read as another user's task.
n.user_id = user_id
n.note_type, n.is_task, n.task_kind = note_type, is_task, task_kind
# The write-path menu reads note.data for a language tag; an auto-mock there
# is truthy and renders its repr into the marker.
n.data = None
return n
# ─── knowledge auto-inject (Path A) ──────────────────────────────────────────
@@ -88,9 +61,9 @@ async def test_build_autoinject_hint_disabled_returns_empty_and_skips_search():
async def test_build_autoinject_hint_titles_only_with_margin_gate():
from scribe.services import plugin_context as pc
# top=0.80; 0.74 within band (0.10), 0.61 outside → dropped.
hits = [(0.80, _note(11, "Pool sizing decision")),
(0.74, _note(22, "run_maintenance thresholds")),
(0.61, _note(33, "unrelated-ish"))]
hits = [(0.80, fake_note(id=11, title="Pool sizing decision", user_id=1)),
(0.74, fake_note(id=22, title="run_maintenance thresholds", user_id=1)),
(0.61, fake_note(id=33, title="unrelated-ish", user_id=1))]
rec = MagicMock()
with patch.object(pc, "get_autoinject_config",
AsyncMock(return_value={"enabled": True, "threshold": 0.55, "top_k": 3})), \
@@ -352,10 +325,10 @@ async def test_a_snippet_takes_the_last_slot_when_none_won_on_score():
records ABOUT building the retrieval system, and zero snippets. Scribe's own
records are about software work, so they share vocabulary with any coding
prompt while answering none of them."""
main = [(0.66, _note(1, "Step 3: title-first auto-inject")),
(0.65, _note(2, "Task-reminder dedup query crashes", is_task=True)),
(0.64, _note(3, "Drafter hardening · write-path trigger", is_task=True))]
reuse = [(0.58, _note(9, "debounce — collapse rapid calls", note_type="snippet"))]
main = [(0.66, fake_note(id=1, title="Step 3: title-first auto-inject", user_id=1)),
(0.65, fake_note(id=2, title="Task-reminder dedup query crashes", user_id=1, is_task=True)),
(0.64, fake_note(id=3, title="Drafter hardening · write-path trigger", user_id=1, is_task=True))]
reuse = [(0.58, fake_note(id=9, title="debounce — collapse rapid calls", user_id=1, note_type="snippet"))]
out, calls = await _autoinject(main, reuse)
@@ -370,8 +343,8 @@ async def test_a_snippet_takes_the_last_slot_when_none_won_on_score():
async def test_the_reserved_query_is_skipped_when_a_snippet_already_won():
"""No second query, and no slot spent twice, when ranking already did the
right thing — the fix must be invisible in the case it isn't needed."""
main = [(0.81, _note(9, "debounce helper", note_type="snippet")),
(0.80, _note(1, "some task", is_task=True))]
main = [(0.81, fake_note(id=9, title="debounce helper", user_id=1, note_type="snippet")),
(0.80, fake_note(id=1, title="some task", user_id=1, is_task=True))]
out, calls = await _autoinject(main, [])
@@ -384,7 +357,7 @@ async def test_a_weak_snippet_does_not_buy_the_slot():
"""The reserved hit skips the MARGIN band — that band is what snippets lose
to — but never the threshold. Silence stays the default; a slot spent on an
irrelevant snippet is how a menu teaches people to ignore it."""
main = [(0.66, _note(1, "a task", is_task=True))]
main = [(0.66, fake_note(id=1, title="a task", user_id=1, is_task=True))]
out, calls = await _autoinject(main, []) # threshold returned nothing
@@ -397,8 +370,8 @@ async def test_the_reserved_hit_is_not_held_to_the_margin_band():
"""0.58 is 0.08 below the top hit. Under the band it would survive; the point
is that it must survive even when it wouldn't — a snippet losing to a
same-vocabulary project record by a wide margin is the whole bug."""
main = [(0.90, _note(1, "a task", is_task=True))]
reuse = [(0.58, _note(9, "debounce", note_type="snippet"))]
main = [(0.90, fake_note(id=1, title="a task", user_id=1, is_task=True))]
reuse = [(0.58, fake_note(id=9, title="debounce", user_id=1, note_type="snippet"))]
out, _calls = await _autoinject(main, reuse)
@@ -409,15 +382,15 @@ async def test_the_reserved_hit_is_not_held_to_the_margin_band():
async def test_a_process_counts_as_reuse_too():
"""A stored process answers 'how do we do X here' the same way a snippet
answers 'what do we already have' — both lose to the same project records."""
main = [(0.70, _note(1, "a task", is_task=True))]
reuse = [(0.60, _note(8, "DRY pass process", note_type="process"))]
main = [(0.70, fake_note(id=1, title="a task", user_id=1, is_task=True))]
reuse = [(0.60, fake_note(id=8, title="DRY pass process", user_id=1, note_type="process"))]
out, _ = await _autoinject(main, reuse)
assert 8 in out["note_ids"]
# …and one already on the menu suppresses the reserved query.
out2, calls2 = await _autoinject(
[(0.70, _note(8, "DRY pass process", note_type="process"))], [])
[(0.70, fake_note(id=8, title="DRY pass process", user_id=1, note_type="process"))], [])
assert len(calls2) == 1
@@ -431,9 +404,8 @@ async def test_write_path_semantic_arm_asks_for_experience_not_just_snippets():
it says what NOT to do."""
from scribe.services import plugin_context as pc
hits = [(0.72, _note(9, "debounce helper", note_type="snippet")),
(0.70, _note(7, "Debounce dropped the trailing call", is_task=True,
task_kind="issue"))]
hits = [(0.72, fake_note(id=9, title="debounce helper", user_id=1, note_type="snippet")),
(0.70, fake_note(id=7, title="Debounce dropped the trailing call", user_id=1, is_task=True, task_kind="issue"))]
search = AsyncMock(return_value=hits)
rec = MagicMock()
with patch.object(pc, "get_writepath_config",
@@ -466,9 +438,8 @@ async def test_write_path_labels_a_non_snippet_hit_with_its_kind():
menu's default and the header's default reading."""
from scribe.services import plugin_context as pc
hits = [(0.72, _note(9, "debounce helper", note_type="snippet")),
(0.71, _note(7, "Debounce dropped the trailing call", is_task=True,
task_kind="issue"))]
hits = [(0.72, fake_note(id=9, title="debounce helper", user_id=1, note_type="snippet")),
(0.71, fake_note(id=7, title="Debounce dropped the trailing call", user_id=1, is_task=True, task_kind="issue"))]
with patch.object(pc, "get_writepath_config",
AsyncMock(return_value={"enabled": True, "threshold": 0.6,
"top_k": 3})), \