dev → main: rule overlap check, design-guidance write arm, usage chip seam, divergence meaning gate #180
@@ -25,6 +25,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.tools import lessons as lesson_tools
|
||||
from scribe.services import note_usage
|
||||
from scribe.services.note_usage import empty_usage
|
||||
|
||||
|
||||
@@ -47,7 +48,7 @@ async def test_the_mcp_listing_carries_usage_for_every_row():
|
||||
new=AsyncMock(return_value=(_rows(), 2))),
|
||||
patch.object(lesson_tools.access_svc, "label_shared_items",
|
||||
new=AsyncMock(side_effect=lambda _uid, items: items)),
|
||||
patch.object(lesson_tools, "usage_for_notes",
|
||||
patch.object(note_usage, "usage_for_notes",
|
||||
new=AsyncMock(return_value=used)),
|
||||
):
|
||||
out = await lesson_tools.list_lessons()
|
||||
@@ -71,7 +72,7 @@ async def test_the_listing_asks_for_usage_once_for_the_whole_page():
|
||||
new=AsyncMock(return_value=(_rows(), 2))),
|
||||
patch.object(lesson_tools.access_svc, "label_shared_items",
|
||||
new=AsyncMock(side_effect=lambda _uid, items: items)),
|
||||
patch.object(lesson_tools, "usage_for_notes", new=reader),
|
||||
patch.object(note_usage, "usage_for_notes", new=reader),
|
||||
):
|
||||
await lesson_tools.list_lessons()
|
||||
|
||||
@@ -101,7 +102,7 @@ async def test_get_lesson_reads_the_count_before_recording_its_own_pull():
|
||||
patch.object(lesson_tools, "_to_dict", return_value={"id": 7}),
|
||||
patch.object(lesson_tools.access_svc, "describe_provenance",
|
||||
new=AsyncMock(return_value={})),
|
||||
patch.object(lesson_tools, "usage_for_notes", new=_usage_read),
|
||||
patch.object(note_usage, "usage_for_notes", new=_usage_read),
|
||||
patch.object(lesson_tools, "record_pulled",
|
||||
side_effect=lambda **_kw: order.append("pull")),
|
||||
):
|
||||
@@ -116,6 +117,13 @@ async def test_get_lesson_reads_the_count_before_recording_its_own_pull():
|
||||
|
||||
# ── the REST door, on structure (rule 167) ───────────────────────────────────
|
||||
#
|
||||
# MOVED 2026-09-21 (#4230), not weakened. These used to look for
|
||||
# `usage_for_notes(` and `empty_usage()` in the route body. Both now live in
|
||||
# ONE seam, `note_usage.attach_usage`, which seven doors share — so the
|
||||
# zero-fill and the single aggregate are pinned once, against the seam, in
|
||||
# tests/test_usage_attach_seam.py. What stays HERE is what only this route can
|
||||
# get wrong: that it calls the seam at all, once, and before it records a pull.
|
||||
#
|
||||
# Its siblings in test_lesson_rest_door.py are source guards for the same
|
||||
# reason: the route is decorated and returns a Quart response, so driving it
|
||||
# means standing up the app. What matters here is reachable from the source
|
||||
@@ -136,19 +144,17 @@ def test_the_rest_listing_reads_usage_once_for_the_page():
|
||||
"""A per-row lookup would be N+1 by construction — the listing's own
|
||||
comment says so, and this is what makes that comment checkable."""
|
||||
src = _route_source("list_lessons_route")
|
||||
assert src.count("usage_for_notes(") == 1
|
||||
# The one call is not inside the loop that assigns the rows.
|
||||
call = src.index("usage_for_notes(")
|
||||
assign = src.index('["usage"]')
|
||||
assert call < assign
|
||||
assert src.count("attach_usage(") == 1
|
||||
assert "attach_usage(items)" in src, "the seam must get the page, not a row"
|
||||
|
||||
|
||||
def test_every_rest_row_carries_the_key_even_at_zero():
|
||||
""""Never surfaced" is a state the UI renders; a missing field is not."""
|
||||
src = _route_source("list_lessons_route")
|
||||
assert "empty_usage()" in src, (
|
||||
"a row with no recorded usage would come back without the key, and a "
|
||||
"reader cannot tell that from a reporting failure"
|
||||
assert "attach_usage(" in src
|
||||
assert 'it["usage"] =' not in src, (
|
||||
"the route re-spells the attach by hand, so a row with no recorded "
|
||||
"usage can come back without the key again"
|
||||
)
|
||||
|
||||
|
||||
@@ -156,7 +162,7 @@ def test_the_rest_detail_door_also_reads_before_it_records():
|
||||
"""Two doors that disagree about what the number counts are worse than
|
||||
one door that is wrong, because only one of them looks wrong."""
|
||||
src = _route_source("get_lesson_route")
|
||||
assert src.index("usage_for_notes(") < src.index("record_pulled(")
|
||||
assert src.index("attach_usage(") < src.index("record_pulled(")
|
||||
|
||||
|
||||
def test_these_guards_can_fail():
|
||||
@@ -181,9 +187,14 @@ def test_the_detail_view_renders_the_badge_rather_than_respelling_it():
|
||||
assert "UsageBadge" in view
|
||||
assert "usage-tag" not in view, "re-spelled the chip instead of reusing it"
|
||||
|
||||
key = 'dead-weight-advice="'
|
||||
start = view.index(key) + len(key)
|
||||
advice = view[start:view.index('"', start)]
|
||||
# The sentence moved out of this template into the shared table (#4230,
|
||||
# recorded on #3460). Assert BOTH halves, so the guard cannot pass by the
|
||||
# view pointing at an entry that no longer says the right thing.
|
||||
assert "DEAD_WEIGHT_ADVICE.lesson" in view, "the view no longer reads the lesson advice"
|
||||
table = (Path(__file__).resolve().parents[1]
|
||||
/ "frontend/src/utils/deadWeight.ts").read_text()
|
||||
start = table.index(" lesson:")
|
||||
advice = table[start:table.index(",\n", start)]
|
||||
assert "when_to_apply" in advice, (
|
||||
f"the dead-weight advice does not point at the trigger: {advice!r}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user