Add runbook pack spec, validator, legacy launcher, and workplans RBT-WP-0002..0006
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
This commit is contained in:
parent
13512e68c8
commit
535836054a
19 changed files with 960 additions and 17 deletions
46
tests/test_legacy.py
Normal file
46
tests/test_legacy.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import socket
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from rtut import legacy, pack as P
|
||||
|
||||
|
||||
def free_port():
|
||||
s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close(); return p
|
||||
|
||||
|
||||
def make_repo(tmp_path, command, url, cwd="."):
|
||||
d = tmp_path / "runbooks" / "old"
|
||||
d.mkdir(parents=True)
|
||||
(d / "pack.yaml").write_text(yaml.safe_dump({
|
||||
"spec": "runbook-pack/v0.1", "id": "t.old", "title": "Old console", "owner": "t",
|
||||
"exercise_status": "unexercised", "engine": "legacy-command",
|
||||
"legacy": {"command": command, "cwd": cwd, "url": url}}))
|
||||
return P.load(d / "pack.yaml")
|
||||
|
||||
|
||||
def test_launch_and_stop(tmp_path):
|
||||
port = free_port()
|
||||
pk = make_repo(tmp_path, [sys.executable, "-m", "http.server", str(port), "--bind", "127.0.0.1"],
|
||||
f"http://127.0.0.1:{port}")
|
||||
proc = legacy.launch(pk, timeout=10)
|
||||
try:
|
||||
assert proc.poll() is None
|
||||
finally:
|
||||
proc.terminate(); proc.wait(5)
|
||||
|
||||
|
||||
def test_timeout_when_console_never_answers(tmp_path):
|
||||
pk = make_repo(tmp_path, [sys.executable, "-c", "import time; time.sleep(30)"],
|
||||
f"http://127.0.0.1:{free_port()}")
|
||||
with pytest.raises(TimeoutError):
|
||||
legacy.launch(pk, timeout=1)
|
||||
|
||||
|
||||
def test_cwd_escape_refused(tmp_path):
|
||||
pk = make_repo(tmp_path, ["true"], "", cwd="../..")
|
||||
with pytest.raises(ValueError):
|
||||
legacy.launch(pk)
|
||||
63
tests/test_pack.py
Normal file
63
tests/test_pack.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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.pop("exercise_status"), "exercise_status"),
|
||||
(lambda d: d.update(exercise_status="done"), "exercise_status"),
|
||||
(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",
|
||||
"exercise_status": "unexercised", "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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue