import copy from pathlib import Path import pytest import yaml from rtut import pack as P ROOT = Path(__file__).resolve().parent.parent GOOD = yaml.safe_load((ROOT / "runbooks/example-hello/pack.yaml").read_text()) def check(tmp_path, data): f = tmp_path / "pack.yaml" f.write_text(yaml.safe_dump(data)) return P.load(f) def test_example_valid(): assert P.load(ROOT / "runbooks/example-hello/pack.yaml").ok def test_discover_finds_pack(): assert any(p.parent.name == "example-hello" for p in P.discover(ROOT)) def mut(fn): d = copy.deepcopy(GOOD); fn(d); return d @pytest.mark.parametrize("fn,frag", [ (lambda d: d["parameters"][0].update(secret=True), "secret"), (lambda d: d["steps"][0].pop("verify"), "verify"), (lambda d: d["steps"][0].update(command="x {{nope}}"), "undefined placeholder"), (lambda d: d["steps"][0].update(risk="destructive"), "requires rollback"), (lambda d: d["steps"][0]["verify"].update(expect="output-contains"), "contains"), (lambda d: d["parameters"][0].update(default="bad host"), "pattern"), (lambda d: d.pop("threat_checks"), "threat_checks"), (lambda d: d["steps"][0].update(description="hvs.ABCDEFGHIJKLMN"), "secret-looking"), (lambda d: d["steps"].append(copy.deepcopy(d["steps"][0])), "duplicate step id"), (lambda d: d["parameters"].append({"id": "extra", "label": "x", "type": "string"}), "never used"), ]) def test_invalid(tmp_path, fn, frag): p = check(tmp_path, mut(fn)) assert not p.ok and any(frag in e for e in p.errors), p.errors def test_legacy_requires_command(tmp_path): d = {"spec": "runbook-pack/v0.1", "id": "x.y", "title": "Legacy x", "owner": "o", "engine": "legacy-command"} assert not check(tmp_path, d).ok d["legacy"] = {"command": ["true"]} assert check(tmp_path, d).ok def test_render(): pk = P.load(ROOT / "runbooks/example-hello/pack.yaml") cmd = pk.data["steps"][0]["command"] assert P.render(cmd, {"host": "example.org", "count": "2"}, pk) == "ping -c 2 example.org" with pytest.raises(ValueError): P.render(cmd, {"host": "a;rm -rf /"}, pk) def test_text_parameter_allows_spaces_but_not_metacharacters(): d = copy.deepcopy(GOOD) d["parameters"].append({"id": "need", "label": "Need", "type": "text", "default": "read a password"}) d["steps"][0]["command"] = 'echo "{{need}}" {{host}} {{count}}' pk = P.Pack(Path("x"), d) assert P.render(d["steps"][0]["command"], {"host": "h"}, pk) == 'echo "read a password" h 1' with pytest.raises(ValueError): P.render(d["steps"][0]["command"], {"host": "h", "need": 'x"; rm -rf /'}, pk) with pytest.raises(ValueError): P.render(d["steps"][0]["command"], {"host": "two words"}, pk) def test_the_deprecated_exercise_status_is_accepted_and_ignored_so_old_packs_still_load(tmp_path): d = copy.deepcopy(GOOD) d["exercise_status"] = "exercised 2026-01-01 by someone" assert check(tmp_path, d).ok d["exercise_status"] = "whatever text" # deprecated: not validated, not used assert check(tmp_path, d).ok d.pop("exercise_status") assert check(tmp_path, d).ok # and it is not required any more