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
+16 -25
View File
@@ -6,16 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime, timezone
import pytest
def _make_mock_session():
s = AsyncMock()
s.__aenter__ = AsyncMock(return_value=s)
s.__aexit__ = AsyncMock(return_value=False)
s.add = MagicMock()
s.commit = AsyncMock()
s.refresh = AsyncMock()
return s
from tests.helpers import make_mock_session
def _fake_rulebook(id=1, owner_user_id=7, title="FabledSword family", description=""):
@@ -35,7 +26,7 @@ def _fake_rulebook(id=1, owner_user_id=7, title="FabledSword family", descriptio
@pytest.mark.asyncio
async def test_create_rulebook_stores_to_db():
mock_session = _make_mock_session()
mock_session = make_mock_session()
with patch("scribe.services.rulebooks.async_session") as mock_cls:
mock_cls.return_value = mock_session
from scribe.services.rulebooks import create_rulebook
@@ -49,7 +40,7 @@ async def test_create_rulebook_stores_to_db():
@pytest.mark.asyncio
async def test_list_rulebooks_returns_owned_only():
rb = _fake_rulebook(id=1)
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = [rb]
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -63,7 +54,7 @@ async def test_list_rulebooks_returns_owned_only():
@pytest.mark.asyncio
async def test_get_rulebook_returns_none_when_not_owner():
"""get_rulebook scopes by owner_user_id — wrong user gets None."""
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -77,7 +68,7 @@ async def test_get_rulebook_returns_none_when_not_owner():
@pytest.mark.asyncio
async def test_update_rulebook_only_sets_provided_fields():
rb = _fake_rulebook(id=1, title="old")
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = rb
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -91,7 +82,7 @@ async def test_update_rulebook_only_sets_provided_fields():
@pytest.mark.asyncio
async def test_delete_rulebook_calls_delete():
rb = _fake_rulebook(id=1)
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = rb
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -124,7 +115,7 @@ def _fake_topic(id=1, rulebook_id=1, title="git-workflow", description="", order
@pytest.mark.asyncio
async def test_create_topic_requires_owned_rulebook():
"""create_topic raises ValueError if the rulebook isn't owned by user."""
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -143,7 +134,7 @@ async def test_list_topics_returns_topics_for_owned_rulebook():
topic = _fake_topic(id=10, rulebook_id=1, title="git-workflow")
# Two execute calls: ownership check, then topic select.
mock_session = _make_mock_session()
mock_session = make_mock_session()
rb_result = MagicMock()
rb_result.scalar_one_or_none.return_value = rb
topic_result = MagicMock()
@@ -182,7 +173,7 @@ def _fake_rule(id=1, topic_id=10, title="dev is home",
@pytest.mark.asyncio
async def test_create_rule_requires_owned_topic():
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None # topic not found
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -199,7 +190,7 @@ async def test_create_rule_requires_owned_topic():
async def test_list_rules_filters_by_topic_id():
"""list_rules(topic_id=X) returns rules in that topic, ownership-scoped."""
rule = _fake_rule(id=1, topic_id=10)
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = [rule]
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -212,7 +203,7 @@ async def test_list_rules_filters_by_topic_id():
@pytest.mark.asyncio
async def test_get_rule_returns_none_when_not_owner():
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -228,7 +219,7 @@ async def test_get_rule_returns_none_when_not_owner():
@pytest.mark.asyncio
async def test_subscribe_project_requires_owned_rulebook():
"""subscribe_project raises if user doesn't own the rulebook."""
mock_session = _make_mock_session()
mock_session = make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
@@ -253,7 +244,7 @@ def _empty():
async def test_get_applicable_rules_returns_shape():
"""get_applicable_rules returns the full projection — including the
new suppression fields and rulebook/topic IDs on each rule."""
mock_session = _make_mock_session()
mock_session = make_mock_session()
sub_result = MagicMock()
sub_result.all.return_value = [(1, "FabledSword family")]
rules_result = MagicMock()
@@ -291,7 +282,7 @@ async def test_get_applicable_rules_returns_shape():
@pytest.mark.asyncio
async def test_get_applicable_rules_truncates_when_over_limit():
"""When limit+1 rows are returned, truncated=True and only `limit` returned."""
mock_session = _make_mock_session()
mock_session = make_mock_session()
sub_result = MagicMock()
sub_result.all.return_value = []
rules_result = MagicMock()
@@ -314,7 +305,7 @@ async def test_get_applicable_rules_truncates_when_over_limit():
@pytest.mark.asyncio
async def test_get_applicable_rules_includes_project_scoped_rules():
"""Project-scoped rules surface in the project_rules field."""
mock_session = _make_mock_session()
mock_session = make_mock_session()
proj_rules_result = MagicMock()
proj_rules_result.all.return_value = [
(100, "Use alembic", "Always run migrations via alembic, never raw SQL."),
@@ -338,7 +329,7 @@ async def test_get_applicable_rules_includes_project_scoped_rules():
async def test_get_applicable_rules_surfaces_suppressed_with_context():
"""Suppressed rules and topics come back with full title + rulebook context
so the UI can render them without an extra round-trip."""
mock_session = _make_mock_session()
mock_session = make_mock_session()
suppressed_rules_result = MagicMock()
suppressed_rules_result.all.return_value = [
# (rule_id, title, topic_id, topic_title, rulebook_id, rulebook_title)