[object Object]

← 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

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 →