"""The write-path design arm: a UI write is told which design system binds it (#4256). A design system binds like a rule, and before this it reached a session only through the session-start block — complete for a session that knows to ask, 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"]