Files
FabledScribe/tests/test_write_path_design_arm.py
T
bvandeusenandClaude Opus 5.5 914f31359c
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 15s
docs(6209): the remaining test files — lessons, reply moment and shape, retrieval specs and scopes, rule doors and judgments, services, snippets, search scope; five tests renamed to their property
Docstrings, section headers and assertion messages drop the record number,
milestone or step that introduced the behaviour, and incident accounts are
restated as the present-tense failure shape. A dead pointer to a design doc
that is not in the repo is removed.

Renamed to the property they check:
  test_the_old_shape_would_be_caught -> test_a_rank_then_scope_query_is_caught
  test_a_ledger_repeat_no_longer_produces_that_case_at_all -> test_a_ledger_repeat_is_counted_not_withheld
  test_the_header_no_longer_promises_once_per_session -> test_the_header_explains_the_seen_marker
  test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file -> test_one_canon_is_not_stamped_onto_a_whole_file
  test_the_repos_own_prose_no_longer_defines_with -> test_the_repos_own_prose_does_not_define_with
  test_the_original_regression_create_note_with_content -> test_the_real_server_refuses_content_on_create_note

Fixture and example ids stay: "note 5" in a supersession chain, "rule 156"
in an illustrative question, `#2275` as the hex-versus-citation case,
`#3244` as the example of a bare number, fixed dates in date-filter tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:44:58 -04:00

212 lines
8.4 KiB
Python

"""The write-path design arm: a UI write is told which design system binds it.
A design system binds like a rule. Delivered only through the session-start
block, it is complete for a session that knows to ask and silent for one
writing a component. These tests pin:
- THE TRIGGER is the file, not a search: a UI path in a project that has a
design system. No vectors, no score, no slot taken from the ranked menu.
- WHAT IT SAYS is an index — each inherited layer's section headings, with a
layer short enough to be a line (the leaf's departure) shown whole.
- IT DOES NOT MOVE THE OTHER ARMS. A design-only write returns without
running the standing-rule arm, which is gated on there being prior art;
letting the design line into that gate would change the rule arm's call
distribution under the floor it was tuned against.
- ONCE PER SESSION PER SYSTEM, on the hook's token-keyed channel.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from scribe.services import plugin_context as pc
from tests.helpers import writepath_cfg
HOUSE = (
"## Aesthetic\n\nModern-mythic with restraint. " + "Long prose. " * 60
+ "\n\n## Where the accent must NOT appear\n\nNot on buttons.\n\n"
"## Voice and tone\n\nPlain language for anything functional."
)
LEAF = "The accent appears on the wordmark and active navigation."
def _design(ds_id=9):
return {
"id": ds_id, "title": "App", "description": "",
"inherits_from": ["House"],
"guidance": [
{"design_system_id": 1, "title": "House", "guidance": HOUSE},
{"design_system_id": ds_id, "title": "App", "guidance": LEAF},
],
"token_count": 3, "token_groups": ["accent"],
}
def _project(ds_id=9):
return MagicMock(id=2, title="App", design_system_id=ds_id)
# ── the trigger ───────────────────────────────────────────────────────────
@pytest.mark.parametrize("path,ui", [
("frontend/src/views/NoteView.vue", True),
("frontend/src/styles/components.css", True),
("web/App.TSX", True),
("templates/index.html", True),
("src/scribe/services/dedup.py", False),
("frontend/src/utils/deadWeight.ts", False),
("README.md", False),
("", False),
])
def test_ui_paths(path, ui):
"""`.ts` is deliberately not UI: a utility module is logic, and firing on
it would put the design line in front of writes it says nothing about."""
assert pc.is_ui_path(path) is ui
@pytest.mark.asyncio
async def test_no_project_asks_nothing():
get = AsyncMock()
with patch.object(pc.projects_svc, "get_project", get):
assert await pc._design_arm(1, 0, "a/B.vue", set()) == ("", "")
get.assert_not_called()
@pytest.mark.asyncio
async def test_a_non_ui_write_asks_nothing():
get = AsyncMock()
with patch.object(pc.projects_svc, "get_project", get):
assert await pc._design_arm(1, 2, "src/x.py", set()) == ("", "")
get.assert_not_called()
@pytest.mark.asyncio
async def test_a_project_without_a_design_system_says_nothing():
ctx = AsyncMock()
with patch.object(pc.projects_svc, "get_project", AsyncMock(return_value=_project(None))), \
patch.object(pc.design_systems_svc, "design_context", ctx):
assert await pc._design_arm(1, 2, "a/B.vue", set()) == ("", "")
ctx.assert_not_called()
@pytest.mark.asyncio
async def test_already_shown_this_session_is_not_fetched_again():
ctx = AsyncMock()
with patch.object(pc.projects_svc, "get_project", AsyncMock(return_value=_project())), \
patch.object(pc.design_systems_svc, "design_context", ctx):
out = await pc._design_arm(1, 2, "a/B.vue", {pc.design_key(9)})
assert out == ("", "")
ctx.assert_not_called()
@pytest.mark.asyncio
async def test_an_unreadable_design_system_says_nothing():
with patch.object(pc.projects_svc, "get_project", AsyncMock(return_value=_project())), \
patch.object(pc.design_systems_svc, "design_context", AsyncMock(return_value=None)):
assert await pc._design_arm(1, 2, "a/B.vue", set()) == ("", "")
@pytest.mark.asyncio
async def test_a_failure_never_breaks_the_write():
with patch.object(pc.projects_svc, "get_project", AsyncMock(side_effect=RuntimeError)):
assert await pc._design_arm(1, 2, "a/B.vue", set()) == ("", "")
# ── what it says ──────────────────────────────────────────────────────────
def test_the_line_indexes_the_house_style_and_inlines_the_departure():
line = pc._design_line("a/B.vue", _design())
assert "App (id 9) (inherits House)" in line
assert "`get_design_system(9)` → `resolved_guidance`" in line
assert "`resolve_design_system(9)`" in line
# The long layer is named by its headings, not pasted.
assert "House covers Aesthetic · Where the accent must NOT appear · Voice and tone" in line
assert "Long prose." not in line
# The short layer is the app's own departure, and is shown whole.
assert f'App: "{LEAF}"' in line
def test_the_line_stays_a_line():
"""The index exists because the prose does not fit: resolved guidance
runs to thousands of characters. A line that grew back to that size
would be the prose again under another name."""
assert len(pc._design_line("a/B.vue", _design())) < 800
def test_a_long_layer_with_no_headings_is_elided_not_pasted():
design = _design()
design["guidance"][0]["guidance"] = "Unheaded prose. " * 200
line = pc._design_line("a/B.vue", design)
assert len(line) < 1200
assert "House:" in line
# ── in the hint ───────────────────────────────────────────────────────────
def _quiet():
"""Every other arm silent: nothing recorded, nothing similar."""
return [
patch.object(pc, "get_writepath_config", AsyncMock(return_value=writepath_cfg())),
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))),
patch.object(pc, "semantic_search_notes", AsyncMock(return_value=[])),
patch.object(pc, "record_retrieval", MagicMock()),
patch.object(pc.projects_svc, "get_project", AsyncMock(return_value=_project())),
patch.object(pc.design_systems_svc, "design_context", AsyncMock(return_value=_design())),
patch.object(pc, "owner_names_for", AsyncMock(return_value={})),
]
async def _hint(patches, **kw):
import contextlib
with contextlib.ExitStack() as stack:
for p in patches:
stack.enter_context(p)
rules = stack.enter_context(
patch.object(pc, "semantic_search_rules", AsyncMock(return_value=[]))
)
out = await pc.build_write_path_hint(1, "frontend/src/B.vue", project_id=2, **kw)
return out, rules
@pytest.mark.asyncio
async def test_a_ui_write_with_no_prior_art_still_hears_the_design_system():
out, _ = await _hint(_quiet())
assert out["context"].startswith("> Design system binds `frontend/src/B.vue`")
assert out["derive_keys"] == [pc.design_key(9)]
assert out["note_ids"] == [] and out["rule_ids"] == []
@pytest.mark.asyncio
async def test_a_design_only_write_does_not_switch_the_rule_arm_on():
"""The rule arm runs only where there is prior art. A design line that
joined that gate would start a semantic rule search on every UI write —
a new population of calls under a floor tuned without them."""
_, rules = await _hint(_quiet())
rules.assert_not_called()
@pytest.mark.asyncio
async def test_shown_once_per_session():
out, _ = await _hint(_quiet(), exclude_derive=[pc.design_key(9)])
assert out["context"] == ""
assert out["derive_keys"] == []
@pytest.mark.asyncio
async def test_beside_prior_art_it_leads_and_rides_the_keyed_channel():
patches = _quiet()
patches[1] = patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=(
[{"id": 5, "title": "fs-button", "user_id": 1, "note_type": "snippet"}], 1,
)))
out, _ = await _hint(patches)
lines = out["context"].splitlines()
assert lines[0].startswith("> Design system binds")
assert any("fs-button" in ln for ln in lines[1:])
assert pc.design_key(9) in out["derive_keys"]
# It takes no menu slot: the snippet is still reported as surfaced.
assert 5 in out["note_ids"]