CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 22s
The write-path rule arm ran only past the notes guard: a write whose code matched no snippet, issue or lesson returned before any rule was searched. It now runs before that guard, on `code or path`, and a write that matched nothing but a rule still returns the rule lines. Operator ruling (2026-10-05): yes. A rule about the file being written does not depend on a snippet resembling it. Cost: one more rule search per write that matched no prior art, with its own embedding (the query is `code or path`, not the notes arm's concept query, so the per-request memo does not cover it). Plus the preference slot from (c) on those writes. Expected telemetry shift, to be read on its own once this reaches main: write_path_rule calls rise toward write_path's count (14 days to 2026-10-22: write_path_rule 4,634 against write_path 3,786 — the notes arm skips trivial payloads, the rule arm does not, so the rule arm will now exceed it). Its zero-result share rises too, since the newly covered writes are the ones that matched nothing in the notes corpus. Tests: the gate test is inverted (a write with no prior art still gets its rule check); the design-only write now runs the rule arm on its path; two notes-arm tests read the write_path row instead of any row, since the rule arm writes its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
8.5 KiB
Python
213 lines
8.5 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 grimoire.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/grimoire/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_write_with_only_a_design_line_still_gets_its_rule_check():
|
|
"""Every write with a path gets the rule check, prior art or not. With
|
|
nothing ranked, the design line still arrives on its own."""
|
|
out, rules = await _hint(_quiet())
|
|
rules.assert_awaited()
|
|
assert rules.await_args_list[0].args[1] == "frontend/src/B.vue"
|
|
assert out["context"].startswith("> Design system binds `frontend/src/B.vue`")
|
|
|
|
|
|
@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"]
|