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
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>
212 lines
8.4 KiB
Python
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"]
|