[object Object]

← back to Terminal Status

stop_verdict: skip null/non-string text blocks (uncaught AttributeError broke the exit-0 invariant); selftest reports a bad fixture header instead of aborting (TK-11921 review)

24de7995010d39b8c0897e34c8ced6053684c553 · 2026-09-18 14:33:04 -0700 · Steve Abrams

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE

Files touched

Diff

commit 24de7995010d39b8c0897e34c8ced6053684c553
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 18 14:33:04 2026 -0700

    stop_verdict: skip null/non-string text blocks (uncaught AttributeError broke the exit-0 invariant); selftest reports a bad fixture header instead of aborting (TK-11921 review)
    
    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
---
 stop_verdict.py      | 18 ++++++++++++++----
 test_stop_verdict.py | 15 +++++++++++++++
 2 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/stop_verdict.py b/stop_verdict.py
index 03cf946..b575dd5 100644
--- a/stop_verdict.py
+++ b/stop_verdict.py
@@ -140,8 +140,9 @@ def closing_text(records):
             parts.append(content)
             continue
         for block in content or []:
-            if isinstance(block, dict) and block.get("type") == "text" and block.get("text", "").strip():
-                parts.append(block["text"])
+            text = block.get("text") if isinstance(block, dict) else None
+            if isinstance(block, dict) and block.get("type") == "text" and isinstance(text, str) and text.strip():
+                parts.append(text)
     return "\n".join(parts)
 
 
@@ -306,7 +307,11 @@ def decide(hits, times, *, base="unknown", base_variant="", base_label="",
         return _out("pink", "set", "P1", label="PARKED" + AUTO, snip=hits["P1"], **meta)
     if "W1" in hits:
         return _out("lightblue", "set", "W1", label="WAITING ON STEVE" + AUTO, snip=hits["W1"], **meta)
-    if "S2" in hits or "S3" in hits:
+    # S2 is a FRESH "next check" in this turn's text -> the session rescheduled itself,
+    # so it legitimately outranks done. S3 is only the PRIOR turn's stored variant; it is
+    # not evidence the session is still monitoring THIS turn, so an explicit done wins over
+    # it (memory done-word-always-leaves-green).
+    if "S2" in hits or ("S3" in hits and not ({"D1", "D2", "D3"} & hits.keys())):
         rule = "S2" if "S2" in hits else "S3"
         t = times.get("S2")
         if t:
@@ -428,7 +433,12 @@ def selftest(out=sys.stdout):
         return 1
     for path in files:
         name = os.path.basename(path)
-        header, _ = load_fixture(path)
+        try:
+            header, _ = load_fixture(path)
+        except (ValueError, KeyError, OSError) as exc:
+            failures += 1
+            print("FAIL %-40s bad fixture header: %s" % (name, exc), file=out)
+            continue
         expect = header.get("expect", {})
         try:
             got = run_fixture(path)
diff --git a/test_stop_verdict.py b/test_stop_verdict.py
index 16f0515..54ff688 100644
--- a/test_stop_verdict.py
+++ b/test_stop_verdict.py
@@ -278,5 +278,20 @@ class CliTests(unittest.TestCase):
         self.assertEqual(r.stdout, "")
 
 
+
+class NullTextBlockTest(unittest.TestCase):
+    """TK-11921 review: a text block whose text is null/non-string must not crash (exit-0 invariant)."""
+
+    def test_null_text_block_is_skipped(self):
+        import json as _json
+        lines = [
+            _json.dumps({"type": "user", "message": {"role": "user", "content": "go"}}),
+            _json.dumps({"type": "assistant", "message": {"content": [
+                {"type": "text", "text": None}, {"type": "text", "text": 42}, {"type": "text", "text": "Done."}]}}),
+        ]
+        d = stop_verdict.verdict_from_lines(lines, base="green", base_variant="", base_label="", idle_pink=True)
+        self.assertEqual(d["verdict"], "pink")
+        self.assertIn(d["rule"], ("D2", "D3"))
+
 if __name__ == "__main__":
     unittest.main()

← ec7cc19 integrations: sync dot-floor.sh with the live TK-11921 Stop  ·  back to Terminal Status  ·  test_stop_verdict: use the sv alias in the null-text regress b5b2887 →