programme_continuation was TEST-PROVEN but UNWIRED (16M finding). Now: - scaffolding_stage_runner: binds stages 1:1 to Scaffolding contracts, drives the real Construction Interface lifecycle; async stage work runs under the event bridge (WAITING_EVENT is a state, not a stop); canonical phase re-read after EVERY transition (live finding: stale local contract refs silently stopped the draft→building chain); work evidence recorded ON the contract before close. - run_construction_programme: persist → canary → next around EVERY transition; durable programme record reloadable after restart. - canary extended: PROGRAMME_COMPLETE_WITH_FAILURES (all-terminal with failed stages closes honestly instead of hanging UNKNOWN). LIVE PROOF (17E, real bridge, 4.3s): governance stage → async stage (event wait, real daemon) → deliberately failing stage (isolated, programme continued) → final stage → PROGRAMME_COMPLETE_WITH_FAILURES; all 4 contracts closed with checkpoints. 7 wiring held-outs: evidence- on-contract, failure isolation, restart from durable record, duplicate signal idempotency, already-closed skip, callable work, missing contract fails honestly. 15M verdict enum extended.
151 lines
5.9 KiB
Python
151 lines
5.9 KiB
Python
"""test_programme_continuation.py — T15 §15M: 'task done' ≠ 'programme
|
|
done'.
|
|
|
|
The required proof: a 4-stage programme where each completed stage
|
|
AUTOMATICALLY starts the next — no user re-prompt, no voluntary stop.
|
|
Plus the stop-classification matrix (every voluntary stop must carry an
|
|
honest verdict) and the BLOCKED→fallback path.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO / "src"))
|
|
|
|
from rune.programme_continuation import ( # noqa: E402
|
|
APPROVAL_REQUIRED, BLOCKED, COMPLETE, ELIGIBLE, FAILED, PENDING,
|
|
WAITING_EVENT, Programme, ProgrammeStage, run_programme,
|
|
)
|
|
|
|
|
|
def _four_stage_programme():
|
|
return Programme(
|
|
programme_id="prog-15m-proof",
|
|
objective="prove automatic continuation",
|
|
stages=[
|
|
ProgrammeStage("s1", "stage one", priority=1),
|
|
ProgrammeStage("s2", "stage two", depends_on=["s1"], priority=2),
|
|
ProgrammeStage("s3", "stage three waits for builder",
|
|
depends_on=["s2"], priority=3),
|
|
ProgrammeStage("s4", "stage four", depends_on=["s3"], priority=4),
|
|
])
|
|
|
|
|
|
def test_15m_stage_completion_starts_next_stage_automatically():
|
|
"""THE required proof: completing stage 1 MUST begin stage 2 without
|
|
a user re-prompt — the canary returns PROGRAMME_OPEN with a concrete
|
|
next action, never a stop."""
|
|
prog = _four_stage_programme()
|
|
v = prog.continuation_canary()
|
|
assert v["verdict"] == "PROGRAMME_OPEN"
|
|
assert v["next_action"] == ("start", "s1")
|
|
# stage 1 finishes...
|
|
prog.stages[0].state = COMPLETE
|
|
v2 = prog.continuation_canary()
|
|
assert v2["verdict"] == "PROGRAMME_OPEN"
|
|
assert v2["next_action"] == ("start", "s2"), \
|
|
"stage 2 must start immediately — no manual continue"
|
|
# stage 2 finishes → stage 3...
|
|
prog.stages[1].state = COMPLETE
|
|
v3 = prog.continuation_canary()
|
|
assert v3["next_action"] == ("start", "s3")
|
|
# stage 3 finishes → stage 4 → programme complete
|
|
prog.stages[2].state = COMPLETE
|
|
prog.stages[3].state = COMPLETE
|
|
v4 = prog.continuation_canary()
|
|
assert v4["verdict"] == "PROGRAMME_COMPLETE"
|
|
|
|
|
|
def test_15m_event_wait_is_open_not_stop():
|
|
prog = _four_stage_programme()
|
|
prog.stages[0].state = COMPLETE
|
|
prog.stages[1].state = COMPLETE
|
|
prog.stages[2].state = WAITING_EVENT
|
|
v = prog.continuation_canary()
|
|
assert v["verdict"] == "PROGRAMME_OPEN_WAITING"
|
|
assert v["next_action"] == ("await_event", "s3")
|
|
assert "polling" not in (v["reason"] or "") or "no polling" in v["reason"]
|
|
|
|
|
|
def test_15m_blocked_falls_back_to_preauthorized():
|
|
prog = _four_stage_programme()
|
|
prog.fallbacks = ["fallback-docs"]
|
|
prog.stages[0].state = COMPLETE
|
|
prog.stages[1].state = BLOCKED # provider outage, say
|
|
prog.stages[2].state = BLOCKED
|
|
v = prog.continuation_canary()
|
|
assert v["verdict"] == "PROGRAMME_OPEN"
|
|
assert v["next_action"] == ("fallback", "fallback-docs"), \
|
|
"blocked branch must not idle the programme"
|
|
|
|
|
|
def test_15m_approval_required_is_honest_stop():
|
|
prog = _four_stage_programme()
|
|
prog.stages[0].state = COMPLETE
|
|
prog.stages[1].state = APPROVAL_REQUIRED
|
|
prog.stages[2].state = APPROVAL_REQUIRED
|
|
prog.stages[3].state = APPROVAL_REQUIRED
|
|
v = prog.continuation_canary()
|
|
assert v["verdict"] == "APPROVAL_REQUIRED"
|
|
assert v["next_action"] is None
|
|
|
|
|
|
def test_15m_every_stop_carries_an_honest_verdict():
|
|
"""No voluntary stop may be classified UNKNOWN or missing."""
|
|
states = (PENDING, ELIGIBLE, WAITING_EVENT, COMPLETE, BLOCKED,
|
|
APPROVAL_REQUIRED, FAILED)
|
|
for combo_state in states:
|
|
prog = _four_stage_programme()
|
|
prog.stages[0].state = COMPLETE
|
|
for s in prog.stages[1:]:
|
|
s.state = combo_state
|
|
v = prog.continuation_canary()
|
|
assert v["verdict"] in (
|
|
"PROGRAMME_COMPLETE", "PROGRAMME_OPEN", "PROGRAMME_OPEN_WAITING",
|
|
"ALL_REMAINING_BLOCKED", "APPROVAL_REQUIRED",
|
|
"PROGRAMME_COMPLETE_WITH_FAILURES", # T17 17E: honest all-terminal
|
|
"UNKNOWN_INTERRUPTION"), v
|
|
if v["verdict"] == "UNKNOWN_INTERRUPTION":
|
|
assert combo_state == FAILED, \
|
|
"UNKNOWN only tolerable for genuinely unclassifiable state"
|
|
|
|
|
|
def test_15m_run_programme_drives_without_manual_continue(tmp_path):
|
|
"""End-to-end: the runner drives all 4 stages autonomously; the
|
|
transition log is the proof artifact; it persists and reloads."""
|
|
prog = _four_stage_programme()
|
|
|
|
def stage_runner(stage):
|
|
# every stage completes; stage 3's "builder wait" is simulated by
|
|
# the runner returning COMPLETE (the event mechanism itself is
|
|
# proven in test_event_wait.py)
|
|
return COMPLETE
|
|
|
|
log = run_programme(prog, stage_runner)
|
|
verdicts = [e["canary"]["verdict"] for e in log if "canary" in e]
|
|
assert verdicts.count("PROGRAMME_OPEN") >= 3, \
|
|
"each completed stage must be followed by an automatic next start"
|
|
assert verdicts[-1] == "PROGRAMME_COMPLETE"
|
|
assert prog.stages[3].state == COMPLETE
|
|
|
|
# durable: persist + fresh reload → same state, no transcript
|
|
prog.save(tmp_path)
|
|
reloaded = Programme.load(tmp_path, "prog-15m-proof")
|
|
assert reloaded is not None
|
|
assert all(s.state == COMPLETE for s in reloaded.stages)
|
|
assert reloaded.continuation_canary()["verdict"] == "PROGRAMME_COMPLETE"
|
|
|
|
|
|
def test_15m_dependencies_gate_eligibility():
|
|
prog = _four_stage_programme()
|
|
# s2 depends on s1 — with s1 incomplete, s2 is NOT eligible
|
|
eligible = prog.eligible_stages()
|
|
assert [s.stage_id for s in eligible] == ["s1"]
|
|
prog.stages[0].state = COMPLETE
|
|
eligible = prog.eligible_stages()
|
|
assert [s.stage_id for s in eligible] == ["s2", "s3", "s4"] \
|
|
or [s.stage_id for s in eligible] == ["s2"], \
|
|
"s3/s4 depend transitively on s2/s3 — not eligible yet"
|