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
+10 -23
View File
@@ -13,6 +13,7 @@ from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from tests.helpers import fake_note
PLUGIN = Path(__file__).resolve().parents[1] / "plugin"
HOOK = PLUGIN / "hooks" / "scribe_prior_art.sh"
@@ -23,20 +24,6 @@ def _snippet_item(nid, title, user_id=1):
return {"id": nid, "title": title, "user_id": user_id, "note_type": "snippet"}
def _note(nid, title, user_id=1, note_type="snippet", is_task=False, task_kind="work"):
n = MagicMock()
n.id, n.title, n.user_id = nid, title, user_id
# Explicitly None, not left to MagicMock's auto-attribute: the semantic arm
# reads `note.data` for the snippet's language (#2244), and an auto-created
# mock there is truthy, so it would render its repr into the menu line.
n.data = None
# Same reasoning, second instance: since #2246 this arm returns issues and
# dev-logs too, so the line names the kind. An auto-mock `is_task` is truthy,
# which would label every snippet here "task".
n.note_type, n.is_task, n.task_kind = note_type, is_task, task_kind
return n
def _cfg(**over):
base = {"enabled": True, "threshold": 0.68, "top_k": 3}
base.update(over)
@@ -218,7 +205,7 @@ async def test_a_failing_location_lookup_does_not_sink_the_hint():
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(side_effect=RuntimeError("boom"))), \
patch.object(pc, "semantic_search_notes",
AsyncMock(return_value=[(0.80, _note(5, "throttle — …"))])), \
AsyncMock(return_value=[(0.80, fake_note(id=5, title="throttle — …", user_id=1, note_type="snippet"))])), \
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})):
out = await pc.build_write_path_hint(1, "src/x.py", code=REAL_CODE)
@@ -249,7 +236,7 @@ async def test_semantic_arm_is_snippet_only_and_browse_scoped():
@pytest.mark.asyncio
async def test_margin_gate_applies_to_the_semantic_arm():
from scribe.services import plugin_context as pc
hits = [(0.80, _note(11, "near")), (0.74, _note(22, "alsoNear")), (0.61, _note(33, "far"))]
hits = [(0.80, fake_note(id=11, title="near", user_id=1, note_type="snippet")), (0.74, fake_note(id=22, title="alsoNear", user_id=1, note_type="snippet")), (0.61, fake_note(id=33, title="far", user_id=1, note_type="snippet"))]
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg(top_k=5))), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
patch.object(pc, "semantic_search_notes", AsyncMock(return_value=hits)), \
@@ -288,7 +275,7 @@ async def test_place_beats_meaning_and_the_cap_covers_both_arms():
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg(top_k=2))), \
patch.object(pc.snippets_svc, "list_snippets", _listing), \
patch.object(pc, "semantic_search_notes",
AsyncMock(return_value=[(0.9, _note(7, "scored"))])), \
AsyncMock(return_value=[(0.9, fake_note(id=7, title="scored", user_id=1, note_type="snippet"))])), \
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})):
out = await pc.build_write_path_hint(1, "src/x.py", code=REAL_CODE)
@@ -363,7 +350,7 @@ async def test_telemetry_uses_its_own_source():
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
patch.object(pc, "semantic_search_notes",
AsyncMock(return_value=[(0.9, _note(7, "scored"))])), \
AsyncMock(return_value=[(0.9, fake_note(id=7, title="scored", user_id=1, note_type="snippet"))])), \
patch.object(pc, "record_retrieval", rec), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})):
await pc.build_write_path_hint(1, "src/x.py", code=REAL_CODE, project_id=4)
@@ -497,7 +484,7 @@ async def test_a_real_helper_clears_the_floor():
"""The floor errs toward keeping recall — anything that plausibly IS a
reusable helper has to get through, or the feature stops working."""
from scribe.services import plugin_context as pc
search = AsyncMock(return_value=[(0.80, _note(5, "debounce — …"))])
search = AsyncMock(return_value=[(0.80, fake_note(id=5, title="debounce — …", user_id=1, note_type="snippet"))])
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
patch.object(pc, "semantic_search_notes", search), \
@@ -712,7 +699,7 @@ async def test_a_cross_language_hit_is_labelled_and_explained():
"""The fail state this closes: an unlabelled Python hit offered while writing
TypeScript is either dismissed as irrelevant or pasted into the .ts file."""
from scribe.services import plugin_context as pc
note = _note(7, "group_pairs — collapse related pairs into groups")
note = fake_note(id=7, title="group_pairs — collapse related pairs into groups", user_id=1, note_type="snippet")
note.data = {"language": "python"}
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
@@ -732,7 +719,7 @@ async def test_a_same_language_hit_is_not_labelled_and_gets_no_preamble():
"""The common case keeps a clean line; the explanation only appears when
there is something on the menu it explains."""
from scribe.services import plugin_context as pc
note = _note(7, "debounce — rate-limit a callback")
note = fake_note(id=7, title="debounce — rate-limit a callback", user_id=1, note_type="snippet")
note.data = {"language": "python"}
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
@@ -749,7 +736,7 @@ async def test_a_same_language_hit_is_not_labelled_and_gets_no_preamble():
async def test_an_unknown_target_extension_never_invents_a_mismatch():
"""A wrong "· python" tag is worse than no tag at all."""
from scribe.services import plugin_context as pc
note = _note(7, "helper — does a thing")
note = fake_note(id=7, title="helper — does a thing", user_id=1, note_type="snippet")
note.data = {"language": "python"}
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
@@ -1054,7 +1041,7 @@ async def test_a_pulled_snippet_already_seen_is_evidence_not_menu():
writes code resembling it. #7 must be scored for this payload — and handed
to the stamp as resemblance — without being re-listed in the menu."""
from scribe.services import plugin_context as pc
search = AsyncMock(return_value=[(0.91, _note(7, "pulled")), (0.80, _note(8, "fresh"))])
search = AsyncMock(return_value=[(0.91, fake_note(id=7, title="pulled", user_id=1, note_type="snippet")), (0.80, fake_note(id=8, title="fresh", user_id=1, note_type="snippet"))])
stamp = AsyncMock(return_value=[{
"path": "src/x.py", "symbol": "debounce", "kind": "sym",
"snippet_id": 7, "reason": "hook: pulled #7; payload resembles it (0.91)",