tankslab.com/logbook/source

tests/test_baton.py

193 lines, exactly as they ship in the download.

#!/usr/bin/env python3 """Prove the baton path does what it claims - and that each check can fail. 1. Tiers are fractions of the window, so they mean the same on a small plan and a large one. 2. The Stop nudge speaks at the right moments, stays quiet when covered, and never loops (stop_hook_active) or speaks in seed mode. 3. A baton goes only to its owner: /clear and compaction of the same process take it, a new process takes it only when the writer is gone, and two waiting are never guessed between. 4. A taken baton is consumed once, and the same session's machine handoff is retired with it, so a later session never opens with a stale copy. 5. A shrug is refused: a handoff has to say something. Each claim is re-run against a deliberately broken copy (MUTANTS at the bottom); a check that passes against broken code is not counted as a check. """ from __future__ import annotations import io import json import os import shutil import subprocess import sys import tempfile from contextlib import redirect_stdout from pathlib import Path BIN = Path(__file__).resolve().parent.parent / "bin" FAILED: list[str] = [] def check(name: str, ok: bool, detail: str = "") -> bool: print(f" {'ok ' if ok else 'FAIL'} {name}" + (f" {detail}" if detail else "")) if not ok: FAILED.append(name) return ok def transcript(path: Path, tok: int) -> None: rows = [{"type": "user", "message": {"role": "user", "content": "keep the logs under /srv/app/logs, never delete them"}}, {"type": "assistant", "message": {"role": "assistant", "content": [{"type": "text", "text": "ok"}], "usage": {"input_tokens": 100, "cache_read_input_tokens": tok - 100, "cache_creation_input_tokens": 0, "output_tokens": 5}}}] path.write_text("\n".join(json.dumps(r) for r in rows) + "\n", encoding="utf-8") BATON = ("# BATON\n## WHAT I AM DOING\nMoving the log rotation to the new volume.\n" "## IN FLIGHT\nrotate.conf edited, not reloaded. Next: reload and watch one rotation.\n" "## EXACT NAMES\n/srv/app/logs, rotate.conf, unit app-rotate.timer\n") def run(bin_dir: Path) -> list[str]: FAILED.clear() tmp = Path(tempfile.mkdtemp(prefix="logbook-baton-test-")) os.environ.update({"LOGBOOK_HOME": str(tmp / "store"), "LOGBOOK_PROJECT": "proj", "LOGBOOK_WINDOW": "200000", "HOME": str(tmp / "home")}) for k in ("LOGBOOK_HANDOFF_MODE", "LOGBOOK_AGENT_PID", "CLAUDE_SESSION_ID", "CLAUDE_TRANSCRIPT_PATH"): os.environ.pop(k, None) for m in ("baton", "handoff", "paths"): sys.modules.pop(m, None) sys.path.insert(0, str(bin_dir)) try: import baton, handoff # noqa: E401 W = 200_000 # 1. tiers as fractions check("under 55% is quiet", baton.tier(100_000, W) == "") check("55% asks", baton.tier(112_000, W) == "ask") check("70% pushes", baton.tier(141_000, W) == "push") check("82% is hard", baton.tier(165_000, W) == "hard") check("same fraction, bigger window, same tier", baton.tier(560_000, 1_000_000) == "ask") check("window grows past 200k when the session visibly does", (os.environ.pop("LOGBOOK_WINDOW") and baton.window(250_000)) == 1_000_000) os.environ["LOGBOOK_WINDOW"] = "200000" # 2. the Stop nudge tr = tmp / "t.jsonl" transcript(tr, 165_000) def stop(payload): buf = io.StringIO() with redirect_stdout(buf): baton.do_stop(payload) return buf.getvalue().strip() p = {"session_id": "aaaa1111", "transcript_path": str(tr)} out = stop(p) check("hard tier with no baton blocks the stop", '"decision": "block"' in out, out[:80]) check("never blocks twice in one stop (stop_hook_active)", stop({**p, "stop_hook_active": True}) == "") handoff.mode_file().write_text("seed", encoding="utf-8") check("seed mode never nudges", stop(p) == "") handoff.mode_file().unlink() check("baton is the default mode", handoff.get_mode() == "baton") transcript(tr, 112_000) st = {} first = baton.nag(112_000, W, {}, st) again = baton.nag(115_000, W, {}, st) later = baton.nag(123_000, W, {}, st) check("ask speaks once, then waits 5% more", bool(first) and again == "" and bool(later)) # 5. a shrug is refused; 3. ownership env = {**os.environ, "CLAUDE_SESSION_ID": "aaaa1111", "CLAUDE_TRANSCRIPT_PATH": str(tr), "LOGBOOK_AGENT_PID": "4242"} r = subprocess.run([sys.executable, str(bin_dir / "baton.py"), "write"], input="ok", text=True, capture_output=True, env=env) check("a two-letter baton is refused", r.returncode == 2) transcript(tr, 150_000) # written at 150k: covers until 150k + 15% of 200k = 180k r = subprocess.run([sys.executable, str(bin_dir / "baton.py"), "write"], input=BATON * 2, text=True, capture_output=True, env=env) check("a real baton is written", r.returncode == 0, r.stderr[-200:]) b = baton.pending("proj") check("exactly one baton on disk", len(b) == 1) body = b[0].read_text(encoding="utf-8") if b else "" check("the machine appendix rides underneath", "ATTACHED BY LOGBOOK" in body and "/srv/app/logs" in body) transcript(tr, 166_000) baton._state_path("aaaa1111").unlink(missing_ok=True) # forget the earlier nudge out = stop(p) check("a written baton covers the session: no hard block, told to hand off", "ready for /clear" in out and "cannot end" not in out, out[:90]) transcript(tr, 185_000) check("past its cover the hard block comes back", "cannot end" in stop(p)) (baton._dir() / "proj--aaaa1111.md").write_text("machine copy", encoding="utf-8") os.environ["LOGBOOK_AGENT_PID"] = "9999" check("/clear in a DIFFERENT process does not take it", baton.take("clear", "bbbb2222") == "") check("resume never takes one", baton.take("resume", "bbbb2222") == "") os.environ["LOGBOOK_AGENT_PID"] = "4242" got = baton.take("clear", "bbbb2222") check("/clear in the SAME process takes it", "Moving the log rotation" in got) check("consumed once", baton.take("clear", "bbbb2222") == "") check("its machine handoff was retired with it", not (baton._dir() / "proj--aaaa1111.md").exists()) # compaction keeps the session id; startup needs the writer gone; two are refused for s in ("cccc3333", "dddd4444"): os.environ["LOGBOOK_AGENT_PID"] = "1" # pid 1 is never the agent: "writer gone" subprocess.run([sys.executable, str(bin_dir / "baton.py"), "write", "--session", s], input=BATON * 2, text=True, capture_output=True, env={**os.environ, "CLAUDE_TRANSCRIPT_PATH": str(tr)}) os.environ["LOGBOOK_AGENT_PID"] = "7777" two = baton.take("startup", "eeee5555") check("two waiting are never guessed between", "NOT DELIVERED" in two and len(baton.pending("proj")) == 2) baton.pending("proj")[0].unlink() check("compaction of the same session takes its own", "Moving" in baton.take("compact", "dddd4444")) finally: sys.path.remove(str(bin_dir)) shutil.rmtree(tmp, ignore_errors=True) return list(FAILED) MUTANTS = [ # (what is broken, file, old, new) - every one must make the suite FAIL ("ask tier ignored", "baton.py", 'else "ask" if f >= ASK else ""', 'else ""'), ("loops on stop_hook_active", "baton.py", 'if p.get("stop_hook_active") or p.get("agent_id"):', 'if p.get("agent_id"):'), ("any process takes a /clear baton", "baton.py", 'pick = [c for c in cands if (pid and _meta(c).get("pid") == str(pid))', 'pick = [c for c in cands if (True)'), ("not consumed", "baton.py", 'p.replace(p.with_name(p.name + ".taken"))', 'pass'), ("shrugs accepted", "baton.py", "if len(text.strip()) < MIN_CHARS:", "if False:"), ("seed is the default", "handoff.py", 'DEFAULT_MODE = "baton"', 'DEFAULT_MODE = "seed"'), ("guesses between two", "baton.py", "if len(pick) != 1:", "if len(pick) == 0:"), ("a baton never covers", "baton.py", "return m if m and tok - int(m.get(\"tokens\", 0) or 0) < COVER * win else {}", "return {}"), ("a baton covers forever", "baton.py", "return m if m and tok - int(m.get(\"tokens\", 0) or 0) < COVER * win else {}", "return m"), ] def main() -> int: print("baton: the real code") bad = run(BIN) print("\nbaton: mutants (each must be caught)") survived = [] for name, f, old, new in MUTANTS: d = Path(tempfile.mkdtemp(prefix="logbook-baton-mut-")) try: shutil.copytree(BIN, d / "bin") p = d / "bin" / f s = p.read_text(encoding="utf-8") if old not in s: survived.append(f"{name} (anchor missing)") continue p.write_text(s.replace(old, new, 1), encoding="utf-8") with redirect_stdout(io.StringIO()): caught = bool(run(d / "bin")) print(f" {'KILLED ' if caught else 'SURVIVED'} {name}") if not caught: survived.append(name) finally: shutil.rmtree(d, ignore_errors=True) if bad or survived: print(f"\nbaton test FAILED: {len(bad)} check(s) failed, {len(survived)} mutant(s) survived") return 1 print(f"\nall baton checks passed; {len(MUTANTS)}/{len(MUTANTS)} mutants caught") return 0 if __name__ == "__main__": raise SystemExit(main())