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
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
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
|