Add lighthouse showcase pack (v0.1 runnable, v0.2 draft); fix manual-check cycle and blocked-step feedback (RBT-WP-0004-T05)
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
f9fb9008bf
commit
b47c948a7e
8 changed files with 387 additions and 12 deletions
|
|
@ -6,7 +6,8 @@ can guide through actual IT administration and operation tasks.
|
|||
- **Intent:** `INTENT.md` · **Scope:** `SCOPE.md`
|
||||
- **Pack specification:** `docs/pack-spec.md`, schema in `schema/`
|
||||
- **Packs live where the procedure lives:** `runbooks/<pack>/pack.yaml` in the owning repo.
|
||||
Examples: `net-kingdom/runbooks/`, and `runbooks/example-hello/` here.
|
||||
Examples: `net-kingdom/runbooks/`, and here `runbooks/example-hello/` and
|
||||
`runbooks/showcase-lighthouse/` (a harmless story to try the UI on).
|
||||
|
||||
```bash
|
||||
python3 -m rtut validate <repo-root> # validate every pack
|
||||
|
|
|
|||
31
rtut/ui.py
31
rtut/ui.py
|
|
@ -79,7 +79,12 @@ class App:
|
|||
if msg:
|
||||
out.append(f"<p class=bad>{e(msg)}</p>")
|
||||
blocked = False
|
||||
for s in d["steps"]:
|
||||
waiting_for = ""
|
||||
total = len(d["steps"])
|
||||
done_n = sum(1 for x in d["steps"] if r.state(x["id"])["state"] in ("done", "skipped"))
|
||||
out.append(f"<p class=eyebrow>{done_n} of {total} steps done</p>"
|
||||
f"<progress max={total} value={done_n} style='width:100%'></progress>")
|
||||
for n, s in enumerate(d["steps"], 1):
|
||||
st = r.state(s["id"]); state = st["state"]
|
||||
act = ""
|
||||
cmd = R.command_for(r, s["id"])
|
||||
|
|
@ -87,18 +92,25 @@ class App:
|
|||
url = f"/run/{i}/{e(rid)}/{e(s['id'])}"
|
||||
v = st.get("verification")
|
||||
if state == "todo" and not blocked:
|
||||
act = f"<form method=post action='{url}/start'>{self.hidden()}<button>Start</button></form>"
|
||||
act = f"<form method=post action='{url}/start'>{self.hidden()}<button>Start this step</button></form>"
|
||||
elif state == "todo":
|
||||
act = f"<p class=note>Waiting for: {e(waiting_for)}</p>"
|
||||
elif state == "running":
|
||||
ver = s["verify"]
|
||||
if ver.get("expect", "manual") == "manual":
|
||||
act = (f"<form method=post action='{url}/verify'>{self.hidden()}<label><input type=checkbox name=ok value=1> "
|
||||
f"I confirm: {e(ver['done_when'])}</label> <button>Record check</button></form>")
|
||||
act = (f"<form method=post action='{url}/verify'>{self.hidden()}"
|
||||
f"<p>Is this true? <b>{e(ver['done_when'])}</b></p>"
|
||||
"<button name=ok value=1>Yes, it is</button> <button name=ok value=0>Not yet</button></form>")
|
||||
else:
|
||||
act = (f"<form method=post action='{url}/verify'>{self.hidden()}<input type=hidden name=yes value=1>"
|
||||
f"<p class=note>Runs: <code>{e(R.render(ver['command'], r.data['params'], p))}</code></p>"
|
||||
f"<button>Run check</button></form>")
|
||||
if v:
|
||||
act += f"<p>{'✔ passed' if v['passed'] else '✘ failed'}</p>"
|
||||
if not v:
|
||||
act = "<p class=note>1. Run the command, then check → 2. Mark done to lock it in</p>" + act
|
||||
elif v["passed"]:
|
||||
act += "<p><b>✔ Check passed.</b> Press <b>Mark done</b> to lock it in; the next step opens after that.</p>"
|
||||
else:
|
||||
act += "<p class=bad>✘ Check not passed. Fix the cause, then check again, or mark the step failed.</p>"
|
||||
if v and v["passed"]:
|
||||
ev = "".join(f"<label>{e(n)} <input name='e_{e(n)}' required></label> " for n in s.get("evidence", []))
|
||||
act += f"<form method=post action='{url}/done'>{self.hidden()}{ev}<button>Mark done</button></form>"
|
||||
|
|
@ -106,10 +118,11 @@ class App:
|
|||
"<button>Mark failed</button></form>")
|
||||
elif state == "failed":
|
||||
act = f"<p class=bad>Failed: {e(st.get('note', ''))}</p><form method=post action='{url}/retry'>{self.hidden()}<button>Retry</button></form>"
|
||||
if state not in ("done", "skipped"):
|
||||
if state not in ("done", "skipped") and not blocked:
|
||||
blocked = True
|
||||
waiting_for = f"step {n}, {s['title']}"
|
||||
rb = f"<p class=note>Rollback: {e(s['rollback'])}</p>" if s.get("rollback") else ""
|
||||
out.append(f"<div class='step {state}'><span class=pill>{state}</span> <b>{e(s['title'])}</b> "
|
||||
out.append(f"<div class='step {state}'><span class=pill>{state}</span> <b>{n}. {e(s['title'])}</b> "
|
||||
f"<span class=note>[{e(s['owner'])}] {e(s.get('risk', 'read-only'))}</span>{block}"
|
||||
f"<p class=note>Done when: {e(s['verify']['done_when'])}</p>{rb}{act}</div>")
|
||||
if r.complete:
|
||||
|
|
@ -167,7 +180,7 @@ def make_handler(app: App, port_holder: list):
|
|||
p = app.packs[i]; r = R.load(p, rid)
|
||||
if action == "start": R.start(r, sid)
|
||||
elif action == "verify":
|
||||
R.verify(r, sid, confirm=bool(form.get("yes")), manual_ok=bool(form.get("ok")) if not form.get("yes") else None)
|
||||
R.verify(r, sid, confirm=bool(form.get("yes")), manual_ok=(form.get("ok") == "1") if not form.get("yes") else None)
|
||||
elif action == "done":
|
||||
R.complete(r, sid, {k[2:]: v for k, v in form.items() if k.startswith("e_")})
|
||||
elif action == "fail": R.fail(r, sid, form.get("note", ""))
|
||||
|
|
|
|||
147
runbooks/showcase-lighthouse/pack.v0.2.draft.yaml
Normal file
147
runbooks/showcase-lighthouse/pack.v0.2.draft.yaml
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# DRAFT - not loaded by the engine. Written against docs/pack-spec-v0.2-proposal.md.
|
||||
# Purpose: an acceptance test for the proposed model. If the model cannot express
|
||||
# this story without special cases, the proposal must change. When v0.2 is adopted
|
||||
# this file becomes pack.yaml (and the v0.1 file is retired or kept as a
|
||||
# compatibility fixture).
|
||||
spec: runbook-pack/v0.2
|
||||
id: rtut.showcase-lighthouse
|
||||
title: "Showcase: a night at the lighthouse"
|
||||
owner: runbook-tutorials
|
||||
outcome: You have played a short interactive story that touches every feature of the runbook engine, using only harmless echo, printf and a scratch file under /tmp/rtut-.
|
||||
exercise_status: unexercised
|
||||
engine: native
|
||||
confirm_phrase: I understand this cannot be undone
|
||||
|
||||
framing: # shown first; things to keep in mind, not gates
|
||||
- This showcase never handles a secret. Datapoints hold references and facts, never secret values.
|
||||
- Only /tmp/rtut-* is ever written or removed.
|
||||
- Several things below can be done in any order; the guide recommends one.
|
||||
|
||||
actors:
|
||||
- id: keeper
|
||||
role: Lighthouse keeper
|
||||
description: Runs the lamp and the signals. Does the everyday work.
|
||||
privileges: [light-lamp, sound-signal, write-logbook]
|
||||
- id: harbourmaster
|
||||
role: Harbour master
|
||||
description: Approves the irreversible closing of the night's log.
|
||||
privileges: [close-log, waive-lamp-check]
|
||||
|
||||
datapoints:
|
||||
- {id: keeper_name, label: Keeper's name, type: string, pattern: "[A-Za-z]+", default: Ada, dimensions: {actor: keeper, kind: identity}, ask_in: dusk}
|
||||
- {id: lamp_fuel, label: Lamp fuel, type: enum, values: [whale-oil, paraffin, electric], default: paraffin, dimensions: {actor: keeper, kind: equipment}, ask_in: dusk}
|
||||
- {id: weather, label: Tonight's weather, type: enum, values: [clear, fog, storm], default: fog, dimensions: {kind: condition}, ask_in: dusk}
|
||||
- {id: motto, label: Motto, type: text, default: Steady light for those at sea, dimensions: {actor: keeper, kind: identity}, ask_in: dusk}
|
||||
- {id: logdir, label: Scratch folder, type: path, pattern: "/tmp/rtut-[a-z0-9-]+", default: /tmp/rtut-lighthouse, dimensions: {kind: location, subsystem: logbook}, ask_in: dusk}
|
||||
- {id: lamp_lit, label: Lamp is lit, type: bool, dimensions: {actor: keeper, kind: state}, produced_by: light-lamp, confirmed_by: lamp-check}
|
||||
|
||||
stages:
|
||||
- id: dusk
|
||||
title: Dusk - prepare
|
||||
goal: The lamp is lit and the weather is known before dark.
|
||||
steps: # no requires among these: they are parallel; list order is the recommended order
|
||||
- id: read-the-sign
|
||||
title: Read the poem over the door
|
||||
actor: keeper
|
||||
owner: the-lighthouse
|
||||
command: printf '%s\n' 'Tower of stone, and a patient flame,' 'you keep no name, yet call each name' 'across the dark, across the foam:' 'come home, come home, come home.'
|
||||
reversible: true
|
||||
verify: {done_when: You read the poem, expect: manual}
|
||||
- id: light-lamp
|
||||
title: Light the lamp
|
||||
actor: keeper
|
||||
owner: the-lighthouse
|
||||
uses: [keeper_name, lamp_fuel]
|
||||
produces: [lamp_lit]
|
||||
command: echo "{{keeper_name}} lights the {{lamp_fuel}} lamp."
|
||||
reversible: true
|
||||
verify: {done_when: The lamp is lit, expect: manual}
|
||||
- id: read-the-sky
|
||||
title: Read the sky
|
||||
actor: keeper
|
||||
owner: the-lighthouse
|
||||
uses: [weather]
|
||||
command: echo "Weather tonight - {{weather}}."
|
||||
reversible: true
|
||||
verify: {done_when: You know the weather, command: 'echo "Weather tonight - {{weather}}."', expect: output-contains, contains: "{{weather}}"}
|
||||
gates:
|
||||
- id: lamp-check
|
||||
title: The lamp burns steadily
|
||||
goal: safe-navigation # quality objective; a canon standard id where one exists
|
||||
blocking: true
|
||||
checks:
|
||||
- {done_when: The light is visible from the water, expect: manual}
|
||||
- {datapoints_ok: [lamp_lit]}
|
||||
waivable: {by: harbourmaster, needs_reason: true}
|
||||
|
||||
- id: night
|
||||
title: Night watch - give warning
|
||||
goal: Ships are warned in a way that suits the weather.
|
||||
requires: [gate:lamp-check]
|
||||
steps:
|
||||
- id: warn
|
||||
title: Warn the ships
|
||||
actor: keeper
|
||||
owner: the-lighthouse
|
||||
uses: [keeper_name, weather]
|
||||
one_of: # alternatives: any one satisfies "warn"; the guide recommends by weather
|
||||
- id: sound-horn
|
||||
title: Sound the fog horn
|
||||
recommended_when: {weather: [fog]}
|
||||
command: echo "{{keeper_name}} sounds the horn through the {{weather}} night."
|
||||
reversible: true
|
||||
verify: {done_when: The horn has sounded, expect: manual}
|
||||
- id: wave-flags
|
||||
title: Signal with flags
|
||||
recommended_when: {weather: [clear]}
|
||||
command: echo "{{keeper_name}} signals with flags in the {{weather}} night."
|
||||
reversible: true
|
||||
verify: {done_when: The flags were seen, expect: manual}
|
||||
- id: shelter
|
||||
title: Bar the door and wait it out
|
||||
recommended_when: {weather: [storm]}
|
||||
command: echo "{{keeper_name}} bars the door. Nobody is warned tonight."
|
||||
reversible: true
|
||||
verify: {done_when: The door is barred, expect: manual}
|
||||
- id: write-logbook
|
||||
title: Write tonight's entry
|
||||
actor: keeper
|
||||
owner: the-lighthouse
|
||||
requires: [warn]
|
||||
uses: [keeper_name, weather, lamp_fuel, motto, logdir]
|
||||
produces: [logbook_file]
|
||||
command: mkdir -p {{logdir}} && echo "Night watch by {{keeper_name}} - {{weather}}, {{lamp_fuel}} lamp. {{motto}}." >> {{logdir}}/logbook.txt
|
||||
reversible: true
|
||||
rollback: rm -rf {{logdir}}
|
||||
verify: {done_when: The logbook exists, command: 'test -s {{logdir}}/logbook.txt', expect: exit-0}
|
||||
|
||||
- id: dawn
|
||||
title: Dawn - close the night
|
||||
goal: The night is on record and the scratch space is clean.
|
||||
requires: [night]
|
||||
steps:
|
||||
- id: close-log
|
||||
title: Close the log for good
|
||||
actor: harbourmaster
|
||||
owner: the-lighthouse
|
||||
uses: [logdir]
|
||||
command: cat {{logdir}}/logbook.txt && rm -rf {{logdir}}
|
||||
reversible: false # engine demands the typed confirm_phrase; no renderer can skip it
|
||||
verify: {done_when: The scratch folder is gone, command: 'test ! -e {{logdir}}', expect: exit-0}
|
||||
gates:
|
||||
- id: night-on-record
|
||||
title: Nothing left behind
|
||||
goal: clean-handover
|
||||
blocking: true
|
||||
checks:
|
||||
- {done_when: No files remain under the scratch folder, command: 'test ! -e {{logdir}}', expect: exit-0}
|
||||
|
||||
artefacts:
|
||||
- id: logbook_file
|
||||
title: The night's logbook entry
|
||||
location: {datapoint: logdir, path: logbook.txt} # a reference, never a value
|
||||
produced_by: write-logbook
|
||||
lives_until: close-log
|
||||
|
||||
ownership:
|
||||
- {concern: The story, owner: runbook-tutorials}
|
||||
102
runbooks/showcase-lighthouse/pack.yaml
Normal file
102
runbooks/showcase-lighthouse/pack.yaml
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
spec: runbook-pack/v0.1
|
||||
id: rtut.showcase-lighthouse
|
||||
title: "Showcase: a night at the lighthouse"
|
||||
owner: runbook-tutorials
|
||||
outcome: You have played a short interactive story that touches every feature of the runbook engine, using only harmless echo, printf and a scratch file. Nothing outside /tmp/rtut-* is touched.
|
||||
exercise_status: unexercised
|
||||
engine: native
|
||||
parameters:
|
||||
- {id: keeper, label: Your keeper's name, type: string, default: Ada, pattern: "[A-Za-z]+", help: One word. Used in the story.}
|
||||
- {id: lamp, label: Lamp fuel, type: enum, values: [whale-oil, paraffin, electric], default: paraffin, help: "Changes only the story text."}
|
||||
- {id: weather, label: Tonight's weather, type: enum, values: [clear, fog, storm], default: fog}
|
||||
- {id: signal, label: Warning signal, type: enum, values: [horn, flags], default: horn, help: "In fog the horn carries; in a storm, flags are lost. Choose well."}
|
||||
- {id: motto, label: The keeper's motto, type: text, default: Steady light for those at sea, help: A short phrase. Spaces are fine; quotes and shell symbols are not.}
|
||||
- {id: logdir, label: Scratch folder for the logbook, type: path, default: /tmp/rtut-lighthouse, pattern: "/tmp/rtut-[a-z0-9-]+", help: Must start with /tmp/rtut-. Removed at the end of the story.}
|
||||
prerequisites:
|
||||
- {text: "A shell with echo, printf, cat and test (any Linux or macOS terminal)", owner: workstation}
|
||||
- {text: "Nothing else. No network, no accounts, no secrets", owner: nobody}
|
||||
steps:
|
||||
- id: read-the-sign
|
||||
title: Read the poem over the door
|
||||
owner: the-lighthouse
|
||||
description: A read-only step with a manual check. The engine cannot know whether you read it, so it asks you.
|
||||
command: |
|
||||
printf '%s\n' 'Tower of stone, and a patient flame,' 'you keep no name, yet call each name' 'across the dark, across the foam:' 'come home, come home, come home.'
|
||||
verify:
|
||||
done_when: You read the poem and know what the light is for
|
||||
expect: manual
|
||||
- id: meet-the-keeper
|
||||
title: Meet the keeper
|
||||
owner: the-lighthouse
|
||||
description: Parameters appear inside commands. Change the name in the parameter form and this line changes.
|
||||
command: echo "{{keeper}} climbs the stairs. Motto on the wall - {{motto}}."
|
||||
verify:
|
||||
done_when: The line names your keeper
|
||||
command: echo "{{keeper}} climbs the stairs. Motto on the wall - {{motto}}."
|
||||
expect: output-contains
|
||||
contains: "{{keeper}} climbs"
|
||||
- id: light-the-lamp
|
||||
title: Light the lamp
|
||||
owner: the-lighthouse
|
||||
command: echo "{{keeper}} lights the {{lamp}} lamp."
|
||||
verify:
|
||||
done_when: The lamp is lit
|
||||
command: echo "{{keeper}} lights the {{lamp}} lamp."
|
||||
expect: output-contains
|
||||
contains: "lights the {{lamp}} lamp"
|
||||
- id: read-the-sky
|
||||
title: Read the sky
|
||||
owner: the-lighthouse
|
||||
command: echo "Weather tonight - {{weather}}."
|
||||
verify:
|
||||
done_when: You know tonight's weather
|
||||
command: echo "Weather tonight - {{weather}}."
|
||||
expect: output-contains
|
||||
contains: "{{weather}}"
|
||||
- id: sound-the-signal
|
||||
title: Sound the warning
|
||||
owner: the-lighthouse
|
||||
description: In a fuller runbook this would be a choice between alternatives. In v0.1 the choice is a parameter.
|
||||
command: echo "{{keeper}} works the {{signal}} through the {{weather}} night."
|
||||
verify:
|
||||
done_when: The signal is given
|
||||
command: echo "{{keeper}} works the {{signal}} through the {{weather}} night."
|
||||
expect: output-contains
|
||||
contains: "works the {{signal}}"
|
||||
- id: write-the-logbook
|
||||
title: Write tonight's entry in the logbook
|
||||
owner: the-lighthouse
|
||||
description: The only step that changes anything, and only a file under /tmp/rtut-. Reversible, so it needs a confirm, not a typed phrase.
|
||||
command: mkdir -p {{logdir}} && echo "Night watch by {{keeper}} - {{weather}}, {{lamp}} lamp, signal {{signal}}. {{motto}}." >> {{logdir}}/logbook.txt
|
||||
risk: changes-state
|
||||
rollback: rm -rf {{logdir}}
|
||||
verify:
|
||||
done_when: The logbook exists and is not empty
|
||||
command: test -s {{logdir}}/logbook.txt
|
||||
expect: exit-0
|
||||
evidence: [logbook_lines]
|
||||
- id: read-the-logbook
|
||||
title: Read the logbook back
|
||||
owner: the-lighthouse
|
||||
command: cat {{logdir}}/logbook.txt
|
||||
verify:
|
||||
done_when: The entry names your keeper
|
||||
command: cat {{logdir}}/logbook.txt
|
||||
expect: output-contains
|
||||
contains: "{{keeper}}"
|
||||
- id: dawn
|
||||
title: Put out the lamp and tidy up
|
||||
owner: the-lighthouse
|
||||
description: Removes the scratch folder. A run in which every step is done can offer to mark this pack exercised.
|
||||
command: rm -rf {{logdir}}
|
||||
risk: changes-state
|
||||
rollback: Repeat write-the-logbook to rebuild the entry.
|
||||
verify:
|
||||
done_when: The scratch folder is gone
|
||||
command: test ! -e {{logdir}}
|
||||
expect: exit-0
|
||||
threat_checks:
|
||||
- This showcase never handles a secret. Parameters are never secret in any runbook; do not type one here to see what happens.
|
||||
- Only /tmp/rtut-* is ever written or removed; the folder pattern is enforced by the parameter.
|
||||
ownership:
|
||||
- {concern: The story, owner: runbook-tutorials}
|
||||
69
tests/test_showcase.py
Normal file
69
tests/test_showcase.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
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 /'})
|
||||
|
|
@ -66,3 +66,31 @@ def test_secret_in_note_refused(srv):
|
|||
req(srv, "POST", loc + "/ping/start", {"_t": t})
|
||||
st, _, body = req(srv, "POST", loc + "/ping/fail", {"_t": t, "note": "hvs.ABCDEFGHIJKLMN"})
|
||||
assert st == 400 and "secret-looking" in body
|
||||
|
||||
|
||||
def test_manual_check_cycle_and_blocked_reason(srv):
|
||||
"""Yes/Not-yet buttons, 'Mark done' to lock in, numbering, and why later steps are blocked."""
|
||||
import shutil
|
||||
t = srv.app.token
|
||||
# use the showcase pack, which starts with a manual step
|
||||
d = Path(srv.app.packs[0].path).parents[2] / "runbooks" / "show"; d.mkdir(parents=True)
|
||||
shutil.copy(ROOT / "runbooks/showcase-lighthouse/pack.yaml", d / "pack.yaml")
|
||||
from rtut import pack as P
|
||||
srv.app.packs.append(P.load(d / "pack.yaml"))
|
||||
i = len(srv.app.packs) - 1
|
||||
_, loc, _ = req(srv, "POST", f"/run/new/{i}", {"_t": t, "operator": "bernd"})
|
||||
_, _, body = req(srv, "GET", loc)
|
||||
assert "0 of 8 steps done" in body and "1. Read the poem" in body
|
||||
assert "Waiting for: step 1" in body # blocked steps explain themselves
|
||||
req(srv, "POST", loc + "/read-the-sign/start", {"_t": t})
|
||||
_, _, body = req(srv, "GET", loc)
|
||||
assert "Yes, it is" in body and "Not yet" in body and "/read-the-sign/done" not in body
|
||||
req(srv, "POST", loc + "/read-the-sign/verify", {"_t": t, "ok": "0"})
|
||||
_, _, body = req(srv, "GET", loc)
|
||||
assert "Check not passed" in body and "/read-the-sign/done" not in body
|
||||
req(srv, "POST", loc + "/read-the-sign/verify", {"_t": t, "ok": "1"})
|
||||
_, _, body = req(srv, "GET", loc)
|
||||
assert "Check passed" in body and "/read-the-sign/done" in body
|
||||
req(srv, "POST", loc + "/read-the-sign/done", {"_t": t})
|
||||
_, _, body = req(srv, "GET", loc)
|
||||
assert "1 of 8 steps done" in body and "Start this step" in body
|
||||
|
|
|
|||
|
|
@ -95,6 +95,21 @@ state_hub_task_id: "dae5a6fe-9f1f-553a-8b73-f47ab6d91b59"
|
|||
Port the four validating packs to v0.2 where it helps, and add v0.2 test packs with
|
||||
alternatives, gates and actors.
|
||||
|
||||
## Showcase pack as acceptance test
|
||||
|
||||
```task
|
||||
id: RBT-WP-0002-T07
|
||||
status: progress
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`runbooks/showcase-lighthouse/` is a harmless interactive-fiction runbook (poem, story,
|
||||
scratch logbook under /tmp/rtut-*). `pack.yaml` (v0.1) runs today and is the test bed for
|
||||
UI refinement. `pack.v0.2.draft.yaml` expresses the same story in the proposed v0.2 model
|
||||
(stages, parallel steps, alternatives, gates, actors, datapoints, artefacts, irreversible
|
||||
confirmation). The draft is checked for internal consistency by tests. When v0.2 is adopted
|
||||
the validator must accept it unchanged, or the proposal changes.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Every pack in this repo and in `net-kingdom` validates.
|
||||
|
|
|
|||
|
|
@ -73,12 +73,12 @@ diff from RBT-WP-0003-T04.
|
|||
|
||||
```task
|
||||
id: RBT-WP-0004-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "d635f972-0377-5581-a8fe-c01ef89559cd"
|
||||
```
|
||||
|
||||
Defects found in review: a ticked manual check shows "passed" while the step stays
|
||||
Done 2026-09-29 against the lighthouse showcase (tested over HTTP; a browser pass by Bernd is still the real check). Defects found in review: a ticked manual check shows "passed" while the step stays
|
||||
`running`, and unticking changes nothing; blocked steps show no Start button and no
|
||||
reason. Make the cycle explicit (check, record, mark done), and show what a blocked
|
||||
step is waiting for.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue