UI for projects, templates, attributes and maturity (RBT-WP-0008-T08); stop a test from writing runs into the repo
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 6s
Work Records / validate (push) Has been cancelled

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-29 13:12:47 +02:00
parent 3470552789
commit eacbf8a21d
9 changed files with 560 additions and 12 deletions

177
tests/test_ui_projects.py Normal file
View file

@ -0,0 +1,177 @@
import http.client
import shutil
import threading
from pathlib import Path
from urllib.parse import urlencode
import pytest
import yaml
from rtut import project as J, run as R, run2, ui
ROOT = Path(__file__).resolve().parent.parent
def write(path: Path, data):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(yaml.safe_dump(data, sort_keys=False))
@pytest.fixture
def psrv(tmp_path):
repo = tmp_path
shutil.copy(ROOT / "runbooks/showcase-lighthouse/pack.yaml", repo / "runbooks/show/pack.yaml") if (repo / "runbooks/show").mkdir(parents=True) is None else None
hello = yaml.safe_load((ROOT / "runbooks/example-hello/pack.yaml").read_text())
hello["parameters"] = [{"id": "host", "label": "Host to ping", "type": "string", "pattern": "[a-zA-Z0-9.-]+"},
{"id": "count", "label": "Ping count", "type": "int", "default": 1}]
write(repo / "runbooks/hello/pack.yaml", hello)
v2 = yaml.safe_load((ROOT / "runbooks/showcase-lighthouse/pack.v0.2.yaml").read_text())
v2["id"] = "rtut.showcase-lighthouse-v2"
write(repo / "runbooks/lh2/pack.yaml", v2)
for f in (ROOT / "attribute-sets").glob("*.yaml"):
write(repo / "attribute-sets" / f.name, yaml.safe_load(f.read_text()))
tpl = yaml.safe_load((ROOT / "project-templates/lighthouse-watch.yaml").read_text())
tpl["runbooks"].append({"pack": "rtut.showcase-lighthouse-v2", "after": ["rtut.example-hello"]})
write(repo / "project-templates/lighthouse-watch.yaml", tpl)
s = ui.serve([repo], 0)
threading.Thread(target=s.serve_forever, daemon=True).start()
s.repo = repo
yield s
s.shutdown()
def req(s, method, path, form=None, host="127.0.0.1"):
c = http.client.HTTPConnection("127.0.0.1", s.server_address[1])
body = urlencode(form) if form is not None else None
c.request(method, path, body, {"Host": host, "Content-Type": "application/x-www-form-urlencoded"})
r = c.getresponse()
return r.status, r.getheader("Location"), r.read().decode()
def make_project(s, pid="harbour", host=None):
t = s.app.token
form = {"_t": t, "project_id": pid, "environment": "rehearsal", "a_keeper": "Ada", "a_logdir": f"/tmp/rtut-ui-{pid}"}
if host:
form["a_host"] = host
return req(s, "POST", "/template/lighthouse-watch/new", form)
def test_index_has_projects_templates_runbooks_and_maturity(psrv):
_, _, body = req(psrv, "GET", "/")
assert "<h1>Projects</h1>" in body and "Project templates" in body and "<h2>Runbooks</h2>" in body
assert "/template/lighthouse-watch" in body
assert ">draft<" in body # maturity pill on every native runbook
def test_template_page_shows_attributes_with_requirement_and_notes(psrv):
st, _, body = req(psrv, "GET", "/template/lighthouse-watch")
assert st == 200 and "A watch at a lighthouse" in body
assert "name='a_keeper'" in body and "Who keeps this light" in body and ">required<" in body
assert "name='a_weather'" not in body # weather is set per target, so it is not a template question
assert "Create project" in body and "<option>rehearsal</option>" in body and "<option>production</option>" in body
assert req(psrv, "GET", "/template/nope")[0] == 404
def test_create_project_from_template_and_view_it(psrv):
st, loc, _ = make_project(psrv)
assert st == 303 and loc == "/project/harbour"
made = yaml.safe_load((psrv.repo / "projects/harbour/project.yaml").read_text())
assert made["template"] == "lighthouse-watch" and made["environment"] == "rehearsal" and made["attributes"]["keeper"] == "Ada"
_, _, body = req(psrv, "GET", "/project/harbour")
assert "A watch at a lighthouse" in body and "from template lighthouse-watch" in body
assert ">planned<" in body and ">rehearsal<" in body
assert "The night watch is played through" in body # the goal
assert "target:north-light" in body # provenance of the fog weather
assert "Still needed" in body and "name='a_host'" in body # hello needs a host
assert "cannot be run in this UI yet" in body # the v0.2 member
def test_needed_attributes_can_be_filled_in_and_are_validated(psrv):
make_project(psrv)
t = psrv.app.token
st, _, body = req(psrv, "POST", "/project/harbour/attributes", {"_t": t, "a_host": "bad host;rm"})
assert st == 400 and "host" in body and "Still needed" in body
assert req(psrv, "POST", "/project/harbour/attributes", {"_t": t, "a_host": "hvs.ABCDEFGHIJKLMN"})[0] == 400
assert req(psrv, "POST", "/project/harbour/attributes", {"_t": t, "a_host": "example.org"})[0] == 303
_, _, body = req(psrv, "GET", "/project/harbour")
assert "Still needed" not in body and "example.org" in body
def test_start_a_run_from_a_project_uses_its_attributes_and_links_back(psrv):
make_project(psrv, host="example.org")
t = psrv.app.token
_, _, body = req(psrv, "GET", "/project/harbour")
assert "waiting for rtut.showcase-lighthouse" in body # hello waits for the showcase
assert body.count("Start run") == 1 # only the member that may start
st, loc, _ = req(psrv, "POST", "/project/harbour/run", {"_t": t, "pack": "rtut.showcase-lighthouse", "target": "north-light", "operator": "bernd"})
assert st == 303 and loc.startswith("/run/") and loc.endswith("#current")
_, _, run_page = req(psrv, "GET", loc.split("#")[0])
assert "project <a href='/project/harbour'>harbour</a> @north-light · rehearsal" in run_page
_, _, body = req(psrv, "GET", "/project/harbour")
assert ">applying<" in body and "latest run" in body
st, loc2, _ = req(psrv, "POST", "/project/harbour/run", {"_t": t, "pack": "rtut.showcase-lighthouse-v2", "operator": "bernd"})
assert st == 303 # the engine accepts it; the UI just cannot walk v0.2 runs yet
assert "cannot walk v0.2 runs yet" in req(psrv, "GET", loc2.split("#")[0])[2]
def test_attribute_table_shows_state_source_modified_and_problems(psrv):
make_project(psrv)
reg_file = psrv.repo / "projects/harbour/project.yaml"
d = yaml.safe_load(reg_file.read_text()); d["attributes"]["logdir"] = "/etc"
reg_file.write_text(yaml.safe_dump(d, sort_keys=False))
_, _, body = req(psrv, "GET", "/project/harbour")
assert "<th>Source</th>" in body and "set:harbour-defaults" in body
assert ">err<" in body and "/etc" in body # invalid value is shown as err, not hidden
assert ">nil<" in body # hello's host has no value yet
def test_review_form_records_a_review_and_raises_maturity(psrv):
t = psrv.app.token
i = psrv.app.pack_index("rtut.showcase-lighthouse")
_, _, body = req(psrv, "GET", f"/pack/{i}")
assert "Maturity" in body and "Reviewed: <b>no</b>" in body and "Record my review" in body
st, loc, _ = req(psrv, "POST", f"/pack/{i}/review", {"_t": t, "by": "alice", "note": "read it"})
assert st == 303 and loc.endswith("#maturity")
_, _, body = req(psrv, "GET", f"/pack/{i}")
assert "Reviewed: <b>yes</b>" in body and "alice" in body and "current revision" in body and ">reviewed<" in body
st, _, body = req(psrv, "POST", f"/pack/{i}/review", {"_t": t, "by": "two words"})
assert st == 400 and "one word" in body
def test_idempotency_shown_per_spec_version(psrv):
i = psrv.app.pack_index("rtut.showcase-lighthouse")
_, _, body = req(psrv, "GET", f"/pack/{i}")
assert "Idempotency: <b>not declared (v0.1 packs cannot declare it)</b>" in body
def test_csrf_host_and_escaping(psrv):
assert req(psrv, "POST", "/template/lighthouse-watch/new", {"project_id": "x", "environment": "rehearsal"})[0] == 403
assert req(psrv, "POST", "/project/harbour/run", {"pack": "x"})[0] == 403
assert req(psrv, "GET", "/project/harbour", host="evil.example")[0] == 403
tpl = psrv.repo / "project-templates/lighthouse-watch.yaml"
d = yaml.safe_load(tpl.read_text()); d["title"] = "<script>alert(1)</script> watch"
tpl.write_text(yaml.safe_dump(d, sort_keys=False))
_, _, body = req(psrv, "GET", "/")
assert "<script>alert(1)" not in body and "&lt;script&gt;alert(1)" in body
_, _, body = req(psrv, "GET", "/template/lighthouse-watch")
assert "<script>alert(1)" not in body
def test_bad_project_id_and_duplicate_are_refused(psrv):
t = psrv.app.token
st, _, body = req(psrv, "POST", "/template/lighthouse-watch/new", {"_t": t, "project_id": "Bad Id!", "environment": "rehearsal"})
assert st == 400 and "project id" in body
assert make_project(psrv)[0] == 303
st, _, body = req(psrv, "POST", "/template/lighthouse-watch/new", {"_t": t, "project_id": "harbour", "environment": "rehearsal"})
assert st == 400 and "already exists" in body
st, _, body = req(psrv, "POST", "/template/lighthouse-watch/new", {"_t": t, "project_id": "other", "environment": "prod"})
assert st == 400 and "environment" in body
def test_v02_run_shows_a_guard_instead_of_crashing(psrv):
reg = psrv.app.registry()
pack = reg.packs["rtut.showcase-lighthouse-v2"]
r = run2.create(pack, "bernd", {"logdir": "/tmp/rtut-ui-v2"})
i = psrv.app.pack_index("rtut.showcase-lighthouse-v2")
st, _, body = req(psrv, "GET", f"/run/{i}/{r.data['run_id']}")
assert st == 200 and "cannot walk v0.2 runs yet" in body