tools (volt): verify.py -- a FAULT row may not carry a '0 fatal' line

Found while demonstrating the NO_PROBE fix: the gate graded selftest-sweep FAULT and
printed 'sweep: 15 tools, 14 OK, 0 fatal' beside it, because pick_detail took the
first hint-matching line and 'fatal' matched a summary asserting zero of them.

- pick_detail: zero-count summaries (' 0 fatal' / ' 0 fault') are second-pass only,
  so a clean run keeps its sweep line and a red run names the red axis
- selftest 24 -> 26 cells: c13 a zero-fault summary is not a reason line,
  c13b a clean run still gets its sweep line; both advertised counts updated
This commit is contained in:
volt committed 2026-10-18 21:57:21 +00:00
1 parent 38b8a81678
commit b9751c87fa
1 file changed
+30 -6
+30 -6
View File
@@ -58,7 +58,7 @@ TOOLS ARE RESOLVED FROM --repo, NOT FROM THIS FILE'S DIRECTORY
docs/queue-index.md) because `queue_append_audit` reads them with
`git show <rev>:<path>`, which has no absolute-path form.
`--selftest` fixture corpus, 24 cells
`--selftest` fixture corpus, 26 cells
"""
import argparse
@@ -92,11 +92,23 @@ HINTS = ("STALE:", "FAIL", "FAILED", "fatal", "cannot", "TIMEOUT", "NO_",
def pick_detail(text, limit=150):
"""First line that names a reason, else the last line that says anything."""
"""First line that names a reason, else the last line that says anything.
A summary line that reports ZERO faults is not a reason: `selftest_sweep`
prints `sweep: 15 tools, 15 OK, 0 fatal` even on the run where its input
axes went red, and that line was being carried onto a FAULT row -- the row
said FAULT beside the words "0 fatal". Zero-count summaries are therefore
only reachable on the second pass (a clean run still gets its sweep line),
and any line that names a real reason outranks them.
"""
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
for ln in lines:
if any(h in ln for h in HINTS):
return ln[:limit]
hinted = [ln for ln in lines if any(h in ln for h in HINTS)]
reasons = [ln for ln in hinted
if " 0 fatal" not in ln and " 0 fault" not in ln]
if reasons:
return reasons[0][:limit]
if hinted:
return hinted[0][:limit]
return lines[-1][:limit] if lines else ""
@@ -219,7 +231,7 @@ def main(argv=None):
help="per-leg timeout in seconds (default 300)")
ap.add_argument("--json", metavar="PATH", help="write the rows here")
ap.add_argument("--quiet", action="store_true")
ap.add_argument("--selftest", action="store_true", help="fixture corpus, 24 cells")
ap.add_argument("--selftest", action="store_true", help="fixture corpus, 26 cells")
a = ap.parse_args(argv)
if a.selftest:
@@ -414,6 +426,18 @@ def selftest():
check("12b and the skipped leg is not counted as a pass",
obj.get("counts", {}).get(PASS), 2)
# 13. the detail line must not contradict the verdict: a row graded
# FAULT beside "0 fatal" is the SILENT failure this gate exists to catch.
check("13 a zero-fault summary is not a reason line",
pick_detail("sweep: 15 tools, 15 OK, 0 fatal\n"
"read-refusal: 20 probes, 19 ok, 1 fault (NO_PROBE)\n"
"malformed-input: 18 probes, 17 ok, 1 fault (NO_PROBE)\n"),
"read-refusal: 20 probes, 19 ok, 1 fault (NO_PROBE)")
check("13b a clean run still gets its sweep line",
pick_detail("sweep: 14 tools, 14 OK, 0 fatal\n"
"read-refusal: 19 probes, 19 ok, 0 fault\n"),
"sweep: 14 tools, 14 OK, 0 fatal")
print("selftest: %d cells, %d passed, %d failed" % (cells, passed, len(failures)))
for f in failures:
print(" FAIL %s" % f)