← back to Terminal Status
TK-11672: fix permanently-red terminal-status test via injectable rescan seam
664d1de1541c48152c65c64a8134cdac2e125c06 · 2026-09-13 23:30:46 -0700 · Steve Abrams
current_owner()'s TK-11398 fresh-rescan escape re-read the LIVE process
table (processes(fresh=True)) instead of the passed-in rows fixture, so
run inside a live claude session it resolved a real owner and the
headless-nested-agent refusal never fired -> test_headless_nested_agent_
cannot_paint_parent was a standing RED, making the 29-test suite unusable
as a gate for the 20 consumer scripts on this engine.
Add an injectable refetch callable (default processes, so production is
byte-identical) threaded through both rescan retries; the test injects
the fixture as the rescan source. Suite now 29/29; negative test proves
SEAM-ON raises, SEAM-OFF returns a live owner (the original bug).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR
Files touched
M terminal_status.pyM test_terminal_status.py
Diff
commit 664d1de1541c48152c65c64a8134cdac2e125c06
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Sep 13 23:30:46 2026 -0700
TK-11672: fix permanently-red terminal-status test via injectable rescan seam
current_owner()'s TK-11398 fresh-rescan escape re-read the LIVE process
table (processes(fresh=True)) instead of the passed-in rows fixture, so
run inside a live claude session it resolved a real owner and the
headless-nested-agent refusal never fired -> test_headless_nested_agent_
cannot_paint_parent was a standing RED, making the 29-test suite unusable
as a gate for the 20 consumer scripts on this engine.
Add an injectable refetch callable (default processes, so production is
byte-identical) threaded through both rescan retries; the test injects
the fixture as the rescan source. Suite now 29/29; negative test proves
SEAM-ON raises, SEAM-OFF returns a live owner (the original bug).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR
---
terminal_status.py | 14 +++++++++++---
test_terminal_status.py | 9 +++++++--
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/terminal_status.py b/terminal_status.py
index efe0a2a..e92291d 100644
--- a/terminal_status.py
+++ b/terminal_status.py
@@ -388,7 +388,15 @@ def owners(rows):
return {tty: group[0] for tty, group in candidates.items() if len(group) == 1}
-def current_owner(rows, _retried=False):
+def current_owner(rows, _retried=False, refetch=processes):
+ # TK-11672: the fresh re-scan below must re-read the SAME source `rows` came
+ # from, not silently escape to the live process table. Production leaves
+ # `refetch` at its default (`processes`) so behaviour is byte-identical; a
+ # test injects `refetch` so the retry re-reads its fixture instead of the
+ # live table (which, run inside a real claude session, would resolve a real
+ # owner and hide the headless-nested-agent refusal this function exists to
+ # make). The seam is guarded by never being passed in production.
+ #
# Resolve ownership by ancestry FIRST. CLAUDE_CODE_CHILD_SESSION is set on
# any tool-spawned shell (the Bash tool, hooks), including a legit top-level
# session's own subshell, so it can't be the sole refusal signal — that made
@@ -406,12 +414,12 @@ def current_owner(rows, _retried=False):
# to _DISK_TTL old can miss a just-started session; re-scan and
# re-decide before refusing.
if not _retried:
- return current_owner(processes(fresh=True), _retried=True)
+ return current_owner(refetch(fresh=True), _retried=True, refetch=refetch)
raise StatusError("Headless or nested agent; refusing parent-terminal write")
# No owning runtime in ancestry at all: only now does the child-session flag
# decide — a true background subagent has no terminal of its own.
if not _retried:
- return current_owner(processes(fresh=True), _retried=True)
+ return current_owner(refetch(fresh=True), _retried=True, refetch=refetch)
if os.environ.get("CLAUDE_CODE_CHILD_SESSION"):
raise StatusError("A subagent has no terminal of its own; refusing to paint")
raise StatusError("No owning Claude/Codex terminal in this process ancestry")
diff --git a/test_terminal_status.py b/test_terminal_status.py
index 8aefb6a..30d9677 100644
--- a/test_terminal_status.py
+++ b/test_terminal_status.py
@@ -336,11 +336,16 @@ class StatusTests(unittest.TestCase):
self.rows[456] = ts.Process(456, 123, "??", self.owner.started, "codex")
self.rows[os.getpid()] = ts.Process(os.getpid(), 456, "??",
self.owner.started, "python3")
+ # TK-11672: inject the fixture as the fresh-rescan source so the retry
+ # cannot escape into the live process table. Without this seam the suite
+ # inherits whatever real session it runs inside and the assertion below
+ # silently passes/fails on live ancestry instead of the fixture.
+ refetch = lambda **_: self.rows
with self.assertRaises(ts.StatusError):
- ts.current_owner(self.rows)
+ ts.current_owner(self.rows, refetch=refetch)
with patch.dict(os.environ, {"CLAUDE_CODE_CHILD_SESSION": "1"}):
with self.assertRaises(ts.StatusError):
- ts.current_owner(self.rows)
+ ts.current_owner(self.rows, refetch=refetch)
def test_paint_failure_is_reported_and_auditable(self):
def fail(*args):
← b85d85b require `export ` + a single declaration before binding a TK
·
back to Terminal Status
·
TK-11665: bind a session's ticket via ledger agent claude@TE 4187d75 →