from pathlib import Path import shutil import subprocess import pytest import yaml from rtut import pack as P, run as R ROOT = Path(__file__).resolve().parent.parent SRC = ROOT / "runbooks/showcase-lighthouse" @pytest.fixture def pk(tmp_path): d = tmp_path / "runbooks" / "showcase"; d.mkdir(parents=True) shutil.copy(SRC / "pack.yaml", d / "pack.yaml") return P.load(d / "pack.yaml") def test_showcase_is_valid(): assert P.load(SRC / "pack.yaml").ok def test_v02_draft_is_wellformed_and_self_consistent(): d = yaml.safe_load((SRC / "pack.v0.2.draft.yaml").read_text()) assert d["spec"] == "runbook-pack/v0.2" actors = {a["id"] for a in d["actors"]} dps = {x["id"] for x in d["datapoints"]} steps = {} for st in d["stages"]: for s in st["steps"]: steps[s["id"]] = s assert s["actor"] in actors for u in s.get("uses", []): assert u in dps, u gates = {g["id"] for st in d["stages"] for g in st.get("gates", [])} stages = {st["id"] for st in d["stages"]} for st in d["stages"]: for r in st.get("requires", []): assert r.removeprefix("gate:") in gates | stages assert any(s.get("reversible") is False for s in steps.values()) assert any("one_of" in s for s in steps.values()) assert not any(k.startswith("secret") for x in d["datapoints"] for k in x) def test_scripted_playthrough(pk): r = R.create(pk, "tester", {"keeper": "Ada", "weather": "storm", "motto": "Light on"}) for s in pk.data["steps"]: sid = s["id"] R.start(r, sid) cmd = R.command_for(r, sid) # the operator runs the command... if cmd: subprocess.run(cmd, shell=True, check=True, capture_output=True) v = s["verify"] # ...then the engine verifies the result res = R.verify(r, sid, confirm=True, manual_ok=True if v["expect"] == "manual" else None) assert res["passed"], sid R.complete(r, sid, {n: "1" for n in s.get("evidence", [])}) assert r.complete assert not Path(r.data["params"]["logdir"]).exists() old, new = R.exercise_update(r) assert "exercised" in new def test_bad_logdir_rejected(pk): with pytest.raises(R.RunError): R.create(pk, "tester", {"logdir": "/etc"}) with pytest.raises(R.RunError): R.create(pk, "tester", {"motto": 'x"; rm -rf /'})