"""Both records show the lesson → rule link in the web UI (milestone 440, #4635). The frontend has no component test runner, so these read the source — and each is written against something the backend owns, so a drift on either side fails here rather than rendering a page that quietly reads wrong: - the client's judgment and link-state unions are the service's and the model's own values, so a renamed state cannot leave a branch that never matches; - the lesson page branches on every judgment, with the unjudged answer as the fall-through, so "not yet judged" is stated rather than left blank; - the write-level list the pages hide controls by is the server's PERMISSION_RANK from editor up, which is the drift that had the snippet page hiding its edit controls from shared editors (`"edit"` is not a level the server sends). """ from __future__ import annotations import re from pathlib import Path from scribe.models.lesson_rule_link import LINK_STATES from scribe.services import lesson_rules as links_svc from scribe.services.access import PERMISSION_RANK FRONTEND = Path(__file__).resolve().parents[1] / "frontend" / "src" def _read(rel: str) -> str: return (FRONTEND / rel).read_text() def _union(source: str, name: str) -> set[str]: """The string members of `export type = "a" | "b" …;`.""" m = re.search(rf"export type {name}\s*=\s*([^;]+);", source) assert m, f"type {name} not found" return set(re.findall(r'"([^"]+)"', m.group(1))) def _template(view: str) -> str: return view[view.index("")] def test_the_client_judgment_union_is_the_services_three_answers(): assert _union(_read("api/lessons.ts"), "RuleJudgment") == { links_svc.LINKED, links_svc.NO_RULE, links_svc.UNJUDGED, } def test_the_client_link_states_are_the_models(): assert _union(_read("api/lessons.ts"), "LinkState") == set(LINK_STATES) def test_the_lesson_page_answers_all_three_judgments(): """`linked` and `no_rule` are tested by name; `unjudged` is the v-else that closes the same chain, so a fourth state the server might add reads as unjudged rather than as nothing.""" tpl = _template(_read("views/LessonDetailView.vue")) linked = tpl.index(f"lesson.rule_judgment === '{links_svc.LINKED}'") no_rule = tpl.index(f"lesson.rule_judgment === '{links_svc.NO_RULE}'") assert linked < no_rule after = tpl[no_rule:] # The no-rule answer renders its reason, and the chain closes on a v-else. para_end = after.index("

") assert "lesson.no_rule?.why" in after[:para_end] closing = after[para_end + len("

"):] assert re.match(r"\s*

")] def test_the_panel_is_gated_on_the_judgment_being_attached(): """An absent `rule_judgment` is "the links could not be read" — never "no rule" — so the panel must not render a state for it.""" tpl = _template(_read("views/LessonDetailView.vue")) assert '

' in src, rel scoped = src[src.index("")] assert ".rule-chip" not in scoped, f"{rel} re-spells the chip" def test_the_write_levels_are_the_servers(): src = _read("utils/permission.ts") m = re.search(r"WRITE_LEVELS[^=]*=\s*\[([^\]]*)\]", src) assert m, "WRITE_LEVELS not found" client = set(re.findall(r'"([^"]+)"', m.group(1))) server = {p for p, rank in PERMISSION_RANK.items() if rank >= PERMISSION_RANK["editor"]} assert client == server def test_no_page_spells_its_own_write_check(): """The helper exists so pages agree; a page comparing against a share level by hand is the copy that drifted to `"edit"`. Matched on the names only a share level uses — `admin` and `owner` are also user roles, and `role === "admin"` is not this.""" offenders = [ str(p.relative_to(FRONTEND)) for p in [*FRONTEND.rglob("*.vue"), *FRONTEND.rglob("*.ts")] if p != FRONTEND / "utils" / "permission.ts" and re.search(r'[!=]==\s*"(?:viewer|editor|edit)"', p.read_text()) ] assert offenders == []