import http.client import shutil import subprocess import threading import uuid from pathlib import Path from urllib.parse import urlencode import pytest from rtut import run as R1, run2 as R, ui ROOT = Path(__file__).resolve().parent.parent PHRASE = "I understand this cannot be undone" _scratch = [] @pytest.fixture def s2(tmp_path): d = tmp_path / "runbooks" / "lh"; d.mkdir(parents=True) shutil.copy(ROOT / "runbooks/showcase-lighthouse-v2/pack.yaml", d / "pack.yaml") s = ui.serve([tmp_path], 0) threading.Thread(target=s.serve_forever, daemon=True).start() yield s s.shutdown() while _scratch: shutil.rmtree(_scratch.pop(), ignore_errors=True) 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 post(s, path, **form): return req(s, "POST", path, {"_t": s.app.token, **form}) def new_run(s, mode="manual", **extra): logdir = f"/tmp/rtut-uv2-{uuid.uuid4().hex[:8]}" _scratch.append(logdir) st, loc, _ = post(s, "/run/new/0", operator="bernd", mode=mode, p_logdir=logdir, **extra) assert st == 303, loc return loc.split("#")[0], logdir def page(s, base, query=""): st, _, body = req(s, "GET", base + query) assert st == 200 return body def do_step(s, base, sid, alt=None, phrase=None, run_cmd=True, cmd_of=None): """What an operator does through the UI: start, do the work, check, mark done.""" form = {} if alt: form["alt"] = alt if phrase: form["phrase"] = phrase assert post(s, f"{base}/{sid}/start", **form)[0] == 303 run = R1.load(s.app.packs[0], base.rsplit("/", 1)[1]) cmd = R.command_for(run, sid) if cmd and run_cmd: subprocess.run(cmd, shell=True, check=True, capture_output=True) eff = R.step_def(run, sid) manual = (eff["verify"].get("expect") or "manual") == "manual" and not eff["verify"].get("datapoints_ok") assert (post(s, f"{base}/{sid}/verify", ok="1") if manual else post(s, f"{base}/{sid}/verify", yes="1"))[0] == 303 assert post(s, f"{base}/{sid}/done")[0] == 303 def pass_gate(s, base, gid): run = R1.load(s.app.packs[0], base.rsplit("/", 1)[1]) for i, ck in enumerate(R.flow(run).gates[gid]["checks"]): exp = ck.get("expect") or (None if ck.get("datapoints_ok") else "manual") assert (post(s, f"{base}/gate/{gid}/check", idx=str(i), ok="1") if exp == "manual" else post(s, f"{base}/gate/{gid}/check", idx=str(i), yes="1"))[0] == 303 # ---- starting ----------------------------------------------------------------------------------------------- def test_v02_pack_is_listed_and_has_a_start_form_with_modes_and_preauthorization(s2): _, _, body = req(s2, "GET", "/") assert "/pack/0" in body and "cannot be run yet" not in body body = page(s2, "/pack/0") assert "Keep in mind" in body and "Start a run" in body assert "name='p_keeper_name'" in body and "actor: keeper" in body # datapoints with their dimension tags assert "name=mode value=manual checked" in body and "name=mode value=auto-confirm" in body assert "Pre-authorize irreversible steps" in body and "name='pre_close-log'" in body and "manual-only" in body assert "name='p_lamp_lit'" not in body # produced datapoints are not questions def test_flow_view_structure_framing_first_next_action_and_parallel_items(s2): base, _ = new_run(s2) body = page(s2, base) assert "Guided flow" in body and "Data" in body and "Overview" in body assert body.index("Keep in mind") < body.index("Dusk - prepare") # framing comes first, not last assert "Manual mode." in body and "Hand over to auto-confirm" in body assert "Next: Read the poem over the door" in body and "(recommended)" in body assert "Also possible in parallel" in body and "#item-light-lamp" in body and "#item-read-the-sky" in body assert "Goal: The lamp is lit" in body assert "Questions for this stage" in body and "name='d_keeper_name'" in body assert "Gate: The lamp burns steadily" in body and "quality goal: safe-navigation" in body and "blocking" in body assert "1. Read the poem over the door" in body and "[Lighthouse keeper · the-lighthouse]" in body assert "