"""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 inspect
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
")]
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 == []
# ── #4658: the web editor records the answer ─────────────────────────────────
def _payload_fn(view: str) -> str:
start = view.index("function answerPayload(")
return view[start:view.index("\n}\n", start)]
def test_the_editor_sends_the_answer_under_the_names_both_routes_read():
"""Rule 33's contract check: a field the form sends under another name is
dropped without an error, and the lesson saves unjudged."""
from scribe.routes import lessons as routes
body = _payload_fn(_read("views/LessonEditorView.vue"))
assert "rule_ids:" in body and "no_rule:" in body
for route in (routes.create_lesson_route, routes.update_lesson_route):
src = inspect.getsource(route)
assert 'data.get("rule_ids")' in src or 'data["rule_ids"]' in src, route.__name__
assert 'data.get("no_rule")' in src, route.__name__
def test_the_editor_offers_the_three_answers():
tpl = _template(_read("views/LessonEditorView.vue"))
offered = set(re.findall(r'v-model="answer" type="radio" value="([a-z_]+)"', tpl))
assert offered == {"rules", "no_rule", "open"}
def test_leaving_it_open_after_linking_clears_the_links_rather_than_nothing():
"""`rule_ids` omitted leaves the links alone; an empty list is what turns a
linked lesson back into an open one. The editor must send the list."""
body = _payload_fn(_read("views/LessonEditorView.vue"))
assert "{ rule_ids: [] }" in body
def test_the_candidates_route_is_where_the_client_asks_and_above_the_id_route():
from scribe.routes import lessons as routes
src = inspect.getsource(routes)
assert src.index('"/rule-candidates"') < src.index('"/