← 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
M stop_verdict.pyM test_stop_verdict.py
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 →