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 "