Files
rune/scripts/tui_startup_canary.py
Nick c2528c1d9d feat(deploy): two-mode TUI canary (candidate scrub vs current), negative deployment regression, release-recursion invariant
Handoff 2026-09-11 Part A-C:

A. tui_startup_canary two modes: candidate (--stage, sys.path SCRUBBED of
   /mnt/workdrive/rune-code/current + imported-rune purge + contamination
   check that rune resolves from the stage) and current (consuming path).
   Scrub proven: candidate mode on the 76f71fc stage reports 76f71fc/0.66.2
   while current is bb536c9/0.66.3. FIXED a live exit-code bug: main()
   returned 1 but was called without sys.exit — a failed canary exited 0
   (the deploy gate's own returncode check in deploy_stage.py was correct,
   so promotion was never actually bypassed; the standalone script was).

B. Negative deployment canary (tests/test_deployment_canary.py): broken
   staged candidate → canary exit 1 naming the failure → current identity
   byte-identical before/after. Plus current-mode identity regression.

C. Release recursion: is_release_commit() extracted into
   version_convergence.py (ONE source of truth); workflow guard uses
   --is-release-subject CLI (no inline python in YAML); regression tests
   cover recognition, never-bump-from-release-commits (incl. the 5x loop
   case), substantive commits still bumping, and the CLI exit contract.

1241 tests green; ratchet 353/80; henxels clean.
2026-09-11 03:16:50 +01:00

119 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""tui_startup_canary.py — REAL TUI initialization canary, two proof modes.
Mode CANDIDATE (pre-promotion, default when --stage is given):
Tests the STAGED candidate exactly as supplied. Nothing from
/mnt/workdrive/rune-code/current is prepended or preferred — sys.path is
scrubbed of any current-deployment entry so a broken candidate cannot be
masked by the healthy previous deployment. If the staged candidate is
broken while current is healthy, this canary MUST fail.
Mode CURRENT (post-promotion consuming-path proof, --mode current):
Tests /mnt/workdrive/rune-code/current/src — proving the promoted
artifact is what the real consuming path resolves to.
Both modes run the ACTUAL RuneApp initialization through Textual's own
test driver — the same app construction + on_mount wiring the interactive
TUI executes — then exit 0. The interactive loop is never entered.
Prints the build identity (mode + identity) on success. Exit non-zero on
any initialization failure.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
from pathlib import Path
CURRENT_SRC = "/mnt/workdrive/rune-code/current/src"
def _scrub_current_from_path() -> None:
"""Remove every current-deployment entry from sys.path (candidate mode).
The runner's checkout workspace or ambient PYTHONPATH may contain the
current deployment; a candidate canary must not silently resolve rune
from it.
"""
sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.abspath(CURRENT_SRC)]
# Also drop any already-imported rune modules from a prior import.
for mod in [m for m in sys.modules if m == "rune" or m.startswith("rune.")]:
del sys.modules[mod]
async def _canary() -> dict:
from textual.widgets import Static # noqa: F401 — imports must resolve
from rune.rune_tui import RuneApp
app = RuneApp()
async with app.run_test(size=(120, 40)) as pilot:
# on_mount has run; the channels/status bar are wired.
assert app.task_channel is not None, "task channel not initialized"
assert app.chat_channel is not None, "chat channel not initialized"
assert app.status_bar is not None, "status bar not initialized"
# One interaction cycle: let pending messages/refreshes settle.
await pilot.pause(0.1)
import rune
return rune.build_info()
def main() -> int:
ap = argparse.ArgumentParser(description="Rune TUI startup canary")
ap.add_argument(
"--mode",
choices=("candidate", "current"),
default="candidate",
help="candidate: test the staged tree (sys.path scrubbed of current); current: test the consuming path",
)
ap.add_argument("--stage", help="staged candidate root (candidate mode)")
args = ap.parse_args()
if args.mode == "candidate":
if not args.stage:
print("candidate mode requires --stage", file=sys.stderr)
return 2
stage_root = Path(args.stage).resolve()
stage_src = str(stage_root / "src")
if not (stage_root / "src" / "rune").is_dir():
print(f"staged candidate has no src/rune: {stage_src}", file=sys.stderr)
return 2
_scrub_current_from_path()
sys.path.insert(0, stage_src)
# Verify the scrub worked: rune must resolve from the STAGE, not current.
import rune # noqa: E402
resolved = getattr(rune, "__file__", "")
if "rune-code/current" in resolved:
print(
f"CANARY CONTAMINATED: rune imported from current, not the staged candidate ({resolved})",
file=sys.stderr,
)
return 2
else:
if CURRENT_SRC not in sys.path:
sys.path.insert(0, CURRENT_SRC)
async def _run() -> dict:
try:
return await _canary()
finally:
pass
try:
info = asyncio.run(_run())
except Exception as exc: # noqa: BLE001 — the canary's JOB is to catch any startup failure and report it as a failed gate; narrowing here would let an unexpected exception class crash the runner instead of failing the gate cleanly.
print(f"TUI STARTUP CANARY FAILED ({args.mode}): {type(exc).__name__}: {exc}", file=sys.stderr)
return 1
print(json.dumps({"mode": args.mode, **info}))
return 0
if __name__ == "__main__":
sys.exit(main())