"""tests-first-with-an-agent-red-green.py — a red-green gate for agent sessions. What it does: enforces the order "the new test fails first, then it passes, then nothing else broke". Two commands, one JSON ledger: python tests-first-with-an-agent-red-green.py red # must FAIL or ERROR python tests-first-with-an-agent-red-green.py green # must pass, then the full # suite must pass too is a unittest id such as test_tally.TallyTests.test_missing_column. The ledger (.redgreen.json in the working directory) records a timestamp for each red and green, and `green` refuses to run for a test that never went red. Run it from the folder that holds the tests; the full-suite step runs `python -m unittest discover -s . -p "test*.py"`. Exit: 0 when the gate passes, 1 when it does not, 2 on usage error. Python 3.13, standard library only. """ from __future__ import annotations import datetime import json import os import subprocess import sys LEDGER = ".redgreen.json" def run_unittest(args: list[str]) -> tuple[int, str]: proc = subprocess.run([sys.executable, "-m", "unittest", *args], capture_output=True, text=True) return proc.returncode, (proc.stdout + proc.stderr) def load() -> dict: if os.path.exists(LEDGER): with open(LEDGER, encoding="utf-8") as f: return json.load(f) return {} def save(ledger: dict) -> None: with open(LEDGER, "w", encoding="utf-8") as f: json.dump(ledger, f, indent=1) f.write("\n") def now() -> str: return datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds") def tail(output: str, n: int = 6) -> str: lines = [ln for ln in output.strip().splitlines() if ln.strip()] return "\n".join(" " + ln for ln in lines[-n:]) def red(test_id: str) -> int: code, out = run_unittest([test_id]) ledger = load() if code == 0: print(f"RED GATE FAILED: {test_id} already passes, so it proves nothing yet.") print(tail(out)) return 1 ledger[test_id] = {"red": now(), "green": None} save(ledger) print(f"RED OK: {test_id} fails as required (exit {code}). Recorded in {LEDGER}.") print(tail(out)) return 0 def green(test_id: str) -> int: ledger = load() entry = ledger.get(test_id) if not entry or not entry.get("red"): print(f"GREEN GATE FAILED: no red run recorded for {test_id}. Run `red` first.") return 1 code, out = run_unittest([test_id]) if code != 0: print(f"GREEN GATE FAILED: {test_id} still fails (exit {code}).") print(tail(out)) return 1 code, out = run_unittest(["discover", "-s", ".", "-p", "test*.py"]) if code != 0: print(f"GREEN GATE FAILED: {test_id} passes but the full suite does not (exit {code}).") print(tail(out)) return 1 entry["green"] = now() save(ledger) print(f"GREEN OK: {test_id} passes and the full suite passes. Ledger: red {entry['red']} -> green {entry['green']}.") print(tail(out)) return 0 def main(argv: list[str]) -> int: if len(argv) != 3 or argv[1] not in ("red", "green"): print(__doc__) return 2 return red(argv[2]) if argv[1] == "red" else green(argv[2]) if __name__ == "__main__": sys.exit(main(sys.argv))