Optional sections: checkpoint tracker, admin/roles, login page

- dashboard.html: each section sits in OPTIONAL:<name> BEGIN/END blocks in
  the style, markup and script, with sample answers so a page with no bot
  shows it working. Core change: setUser records the role on <body>, so CSS
  hides admin-only parts from operators.
- templates/sections.py keeps the chosen sections and deletes the rest,
  markers included; it stops on a broken marker.
- templates/sections.md: what each section needs from the host (events,
  actions, desktop slot snippets, the role re-check, the login hand-off).
- SKILL.md step 5 asks one multi-select question and runs sections.py.
- Page check: checkpoint states, admin visibility by role and assign/remove,
  login theme and hand-off to auth or the stub. Cases skip on a generated
  page without the section; the template must carry all three. The harness
  signs in when the page has a login screen.
- tests/test_sections.py: no sections leave no trace, and that page still
  passes the page check.
- Eval runner: a-empty-sections variant (all three) and per-section checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-16 14:31:25 +05:30
1 parent 3eb3fdf00e
commit 9a5b95448d
10 files changed
+536 -10

No files matched your search

+5
View File
@@ -17,6 +17,11 @@ uv run --with pytest --with playwright pytest tests
`window.__calls` as `[slot, ...args]`.
- To add a case, write a `test_*.py` file in `page_check/` that uses the `page`
and `push(event, data)` fixtures from `conftest.py`.
- Optional-section cases (`test_optional_sections.py`) skip on a generated page
built without that section. The template must carry all of them. A page with
the login section is signed in (`login_result ok`) before each case.
- `test_sections.py`: the optional-section strip tool keeps only the chosen
sections, and a page with none of them still passes the page check.
- `evals/`: **Seam 2.** Runs the skills on a fixture project with `claude -p`,
then grades the reply and runs the page check on the page it built:
+22 -1
View File
@@ -35,6 +35,18 @@ FIXTURES = {
"asks": [r"colou?r", r"desktop|web", r"section"],
"never_asks": [r"\bkeep\b|restyle"],
"brand": "#7a3cff",
"sections": [],
},
"a-empty-sections": {
"dir": "a-empty",
"prompt": "Build me an RPA dashboard for this project. I can't answer questions while you "
"work, so here are my answers to anything the skill asks. Colours: Superbill Ember. "
"Desktop or web: web. Optional sections: all three (checkpoint tracker, admin/roles "
"screen, login page). The project has no auth yet.",
"frontend": r"^\s*(none|no\b)",
"asks": [r"colou?r", r"desktop|web", r"section"],
"never_asks": [r"\bkeep\b|restyle"],
"sections": ["checkpoints", "admin", "login"],
},
}
@@ -43,7 +55,7 @@ def main(fixture):
cfg = FIXTURES[fixture]
work = Path(tempfile.mkdtemp(prefix=f"rpa-eval-{fixture}-"))
project = work / "project"
shutil.copytree(ROOT / "tests" / "fixtures" / fixture, project)
shutil.copytree(ROOT / "tests" / "fixtures" / cfg.get("dir", fixture), project)
for skill in ROOT.glob("rpa-dashboard-*"):
shutil.copytree(skill, project / ".claude" / "skills" / skill.name)
@@ -80,10 +92,19 @@ def main(fixture):
spec.loader.exec_module(palette)
t = palette.derive(cfg["brand"])
checks["custom palette applied"] = all(f"--accent:{t[m]['--accent']}" in html for m in t)
if "sections" in cfg: # chosen sections built, the others leave no trace
spec = importlib.util.spec_from_file_location("test_sections", ROOT / "tests" / "test_sections.py")
traces = importlib.util.module_from_spec(spec)
spec.loader.exec_module(traces)
for name, trace in traces.TRACES.items():
want = name in cfg["sections"]
checks[f"{name} {'built' if want else 'left no trace'}"] = bool(trace.search(html)) == want
pc = subprocess.run([sys.executable, "-m", "pytest", str(ROOT / "tests" / "page_check"), "-q"],
env={**os.environ, "PAGE_CHECK_HTML": str(pages[0])}, capture_output=True, text=True)
checks["page check passes"] = pc.returncode == 0
page_check = pc.stdout.strip().splitlines()[-1:] if pc.stdout else [pc.stderr[-500:]]
if cfg.get("sections"): # every chosen section's page check ran
checks["no section checks skipped"] = "skipped" not in page_check[0]
else:
page_check = ["no page"]
+3
View File
@@ -22,6 +22,7 @@ TEST_FILL = {
"API_BASE": "",
"STEPS_JSON": json.dumps(["Initializing", "Login", "Process items", "Completed"]),
"REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}]),
"CHECKPOINTS_JSON": json.dumps(["Sign in", "Open the list", "Process items", "Finish up"]),
}
FAKE_BRIDGE = """
window.__calls = [];
@@ -63,6 +64,8 @@ def page(request, browser, page_url):
pg.on("pageerror", lambda e: errors.append(e))
pg.goto(page_url)
assert not errors, f"page script errors: {errors}"
if pg.get_by_role("form", name="Sign in").is_visible(): # login page section: the host lets us in
push_to(pg, "login_result", {"ok": True})
if dark:
push_to(pg, "apply_dark", {"on": True})
yield pg
+146
View File
@@ -0,0 +1,146 @@
"""Optional sections (see templates/sections.md). On a generated page, a case skips when the
page was built without its section. The template itself must carry every section."""
import os
import re
import pytest
from playwright.sync_api import expect
from test_contract import colour_of, open_tab
def need(locator, section):
if not locator.count():
if not os.environ.get("PAGE_CHECK_HTML"):
pytest.fail(f"the template has no {section}")
pytest.skip(f"page built without the {section}")
return locator
def fits_width(page, locator):
box = locator.bounding_box()
return box["x"] >= 0 and box["x"] + box["width"] <= page.viewport_size["width"]
CP_SHOWS = {"pending": ("Pending", "grey"), "active": ("In progress", "amber"),
"done": ("Done", "green"), "stuck": ("Stuck", "red")}
def test_checkpoint_boxes_show_each_state(page, push):
track = need(page.get_by_role("list", name="Checkpoints", include_hidden=True), "checkpoint tracker")
open_tab(page, "Run")
boxes = track.get_by_role("listitem")
n = boxes.count()
assert n >= 2, "the tracker needs at least two checkpoints"
def expect_boxes(*kinds):
for i, kind in enumerate(kinds):
label, colour = CP_SHOWS[kind]
expect(boxes.nth(i)).to_have_text(re.compile(rf"^\s*{i + 1}(?!\d).*{label}\s*$"))
assert colour_of(boxes.nth(i)) == colour, (i + 1, kind)
expect_boxes(*["pending"] * n)
push("cpstate", {"kind": "active", "cp": 2}) # earlier boxes are done, later ones pending
expect_boxes("done", "active", *["pending"] * (n - 2))
push("cpstate", {"kind": "stuck", "cp": 2})
expect_boxes("done", "stuck")
push("cpstate", {"kind": "done", "cp": 2})
expect_boxes("done", "done")
push("cpstate", {"kind": "pending", "cp": 2})
expect_boxes("done", "pending")
assert fits_width(page, boxes.last)
ADA = {"username": "ada", "emp_id": "E1", "role": "admin"}
OLIVE = {"username": "olive", "emp_id": "E2", "role": "operator"}
def admin_nav(page):
return need(page.get_by_role("navigation").get_by_role("button", name="Admin", exact=True, include_hidden=True),
"admin screen")
def test_admin_screen_only_for_admins(page, push):
nav = admin_nav(page)
push("set_user", OLIVE)
expect(nav).to_be_hidden()
push("set_user", ADA)
expect(nav).to_be_visible()
nav.click()
users = page.get_by_role("table", name="Users with access")
push("admin_data", {"rows": [ADA, OLIVE]})
rows = users.locator("tbody").get_by_role("row")
expect(rows).to_have_count(2)
expect(rows.nth(1)).to_contain_text("olive")
expect(rows.nth(1)).to_contain_text(re.compile("operator", re.I))
push("set_user", OLIVE) # operators see neither the nav item nor the screen
expect(nav).to_be_hidden()
expect(users).to_be_hidden()
def test_admin_assigns_and_removes_roles(page, push):
nav = admin_nav(page)
push("set_user", ADA)
nav.click()
sample = page.evaluate("SAMPLE_DATA")
form = page.get_by_role("form", name="Assign access")
form.get_by_role("textbox", name="Username or employee ID").fill("grace")
form.get_by_role("combobox", name="Role").select_option("admin")
form.get_by_role("button", name="Assign").click()
rows = page.get_by_role("table", name="Users with access").locator("tbody").get_by_role("row")
if not sample: # wired page: the host answers with fresh admin_data
assert ["assign_role", "grace", "admin"] in page.evaluate("window.__calls")
push("admin_data", {"rows": [ADA, {"username": "grace", "emp_id": "", "role": "admin"}]})
grace = rows.filter(has_text="grace")
expect(grace).to_contain_text(re.compile("admin", re.I))
grace.get_by_role("button", name="Remove").click()
if not sample:
assert ["remove_role", "grace"] in page.evaluate("window.__calls")
push("admin_data", {"rows": [ADA]})
expect(grace).to_have_count(0)
def login_form(page):
return need(page.get_by_role("form", name="Sign in", include_hidden=True), "login page")
def sign_out(page):
page.get_by_role("button", name="Sign out", exact=True).click()
def light_text(locator):
rgb = re.findall(r"\d+", locator.evaluate("el => getComputedStyle(el).color"))[:3]
return sum(int(x) for x in rgb) / 3 > 128
def test_login_page_renders_in_the_theme(page, request):
form = login_form(page)
sign_out(page)
expect(form).to_be_visible()
assert fits_width(page, form)
assert light_text(form) == (request.node.callspec.params["page"] == "dark")
expect(form.get_by_role("button", name="Sign in")).to_be_visible()
def test_login_hands_off_to_auth_or_stub(page, push):
form = login_form(page)
sample = page.evaluate("SAMPLE_DATA")
sign_out(page)
form.get_by_role("textbox", name="Username").fill("alice")
form.get_by_label("Password").fill("secret")
form.get_by_role("button", name="Sign in").click()
if sample: # no auth yet: the stub signs anyone in
expect(form).to_be_hidden()
expect(page.get_by_text("alice", exact=True)).to_have_count(1)
sign_out(page)
else: # the host checks it with the project's auth
calls = page.evaluate("window.__calls")
assert ["logout"] in calls and ["login", "alice", "secret"] in calls
push("login_result", {"ok": False, "msg": "Wrong password"})
expect(form.get_by_role("alert")).to_have_text("Wrong password")
expect(form).to_be_visible()
push("login_result", {"ok": True, "user": {"username": "bob", "emp_id": "7", "role": "operator"}})
expect(form).to_be_hidden()
expect(page.get_by_text("bob", exact=True)).to_have_count(1)
+55
View File
@@ -0,0 +1,55 @@
"""Optional sections: the strip tool keeps the chosen sections and leaves nothing of the rest."""
import importlib.util
import json
import os
import re
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
TEMPLATES = ROOT / "rpa-dashboard-no-ai" / "templates"
spec = importlib.util.spec_from_file_location("sections", TEMPLATES / "sections.py")
sections = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sections)
TEMPLATE = (TEMPLATES / "dashboard.html").read_text(encoding="utf-8")
# Text each optional section brings into the page (markup, script or style).
TRACES = {
"checkpoints": re.compile(r"checkpoint|cpstate|cp-", re.I),
"admin": re.compile(r"admin", re.I),
"login": re.compile(r"login_|sign in|sign out|logout", re.I),
}
def test_no_sections_leave_nothing_behind():
html = sections.keep(TEMPLATE, [])
for name, trace in TRACES.items():
assert not trace.search(html), f"{name} left behind: {trace.search(html)[0]!r}"
assert "OPTIONAL" not in html
@pytest.mark.parametrize("chosen", [["checkpoints"], ["admin"], ["login"], ["checkpoints", "admin", "login"]])
def test_chosen_sections_are_kept_and_the_rest_removed(chosen):
html = sections.keep(TEMPLATE, chosen)
for name, trace in TRACES.items():
assert bool(trace.search(html)) == (name in chosen), name
assert "OPTIONAL" not in html
def test_unknown_section_is_refused():
with pytest.raises(SystemExit, match="checkpoints, admin, login"):
sections.keep(TEMPLATE, ["charts"])
def test_page_without_sections_passes_the_page_check(tmp_path):
fill = {"APP_NAME": "T", "APP_SUBTITLE": "", "LOGO_SRC": "", "API_BASE": "",
"STEPS_JSON": json.dumps(["Start", "Work", "Done"]),
"REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}])}
page_file = tmp_path / "dashboard.html"
page_file.write_text(re.sub(r"\{\{(\w+)\}\}", lambda m: fill[m[1]], sections.keep(TEMPLATE, [])), encoding="utf-8")
r = subprocess.run([sys.executable, "-m", "pytest", str(ROOT / "tests" / "page_check"), "-q", "-p", "no:cacheprovider"],
env={**os.environ, "PAGE_CHECK_HTML": str(page_file)}, capture_output=True, text=True)
assert r.returncode == 0, r.stdout[-2000:]