Files
felhom.eu/scripts/test_due_checks_gate.py
T
admin 104ef34f57
gates / gates (push) Successful in 15s
gates: the today-override announces itself; malformed no longer swallowed
Part 6 of the hub-blindness task, separable and done rather than dropped.

Both due_checks_gate.py and instructions_gate.py read FELHOM_GATE_TODAY so
their suites can control "today", and neither said so. A shell that still has
it exported -- exactly what a session doing gate-test work leaves behind --
made both gates evaluate against a fabricated date and pass in SILENCE. That
is this project's own named failure class: an instrument that can quietly
return the wrong answer is not a measurement. The seam is legitimate and
stays; the silence was the defect.

Both now print a loud line naming the variable, its value, and that the real
date is being ignored, before any verdict.

And instructions_gate.py no longer swallows a MALFORMED override: it used to
fall through to the real date without a word while due_checks_gate.py already
exited 2 on the same input -- one variable, two gates, disagreeing about what
a mistake means. Both exit 2 now.

Tests extended in both suites (42 and 73 assertions, green). Red-proof: the
announcement was deleted from due_checks_gate.py and its two assertions were
seen failing, then reverted.

Also adds REPORT-hub-blindness.md (topic-suffixed; the shared REPORT.md is
left alone per the parallel-session rule).
2026-08-18 19:32:35 +02:00

251 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""Fixture tests for due_checks_gate.py.
Run: python3 scripts/test_due_checks_gate.py
Every test asserts the EFFECT — the exit code AND that the message names the item and the reason —
not merely that the gate ran. A gate that refuses a push without saying WHICH check is overdue sends
the reader back to the register to guess, which is the state this gate exists to end.
Group G drives `repo_gates.py` itself and asserts the gate appears in the runner's output. It
deliberately does NOT grep repo_gates.py's source: a commented-out registration still contains the
string, so a source grep would pass on exactly the inert-seam failure this project has shipped
before. Only running the runner proves the gate is wired.
Fixtures are temp files; nothing here touches the real register.
"""
import os
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
GATE = os.path.join(HERE, "due_checks_gate.py")
RUNNER = os.path.join(HERE, "repo_gates.py")
PASSED = []
FAILED = []
def check(name, cond, detail=""):
(PASSED if cond else FAILED).append(name + ((" — " + detail) if detail else ""))
print((" PASS " if cond else " FAIL ") + name + ((" " + detail) if detail else ""))
def run_gate(register_path, today="2026-08-20"):
env = dict(os.environ, FELHOM_GATE_TODAY=today)
p = subprocess.run([sys.executable, GATE, register_path],
capture_output=True, text=True, env=env)
return p.returncode, p.stdout + p.stderr
HEADER = "| item | due (UTC) | what to measure |\n|---|---|---|\n"
def make_register(tmp, rows_block, item_rows=("R-341",), markers=1, name="OPEN-ITEMS.md"):
"""Build a fixture register: some R-rows, then `markers` copies of the delimited block."""
path = os.path.join(tmp, name)
body = ["# OPEN-ITEMS (fixture)", ""]
for it in item_rows:
body.append("| **%s** | some description | WATCHING | — | do a thing | CC |" % it)
body.append("")
for _ in range(markers):
body.append("<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.")
body.append(" One row per dated check. Dates are UTC. -->")
body.append(rows_block.rstrip("\n"))
body.append("<!-- DUE-CHECKS-END -->")
body.append("")
with open(path, "w", encoding="utf-8") as fh:
fh.write("\n".join(body) + "\n")
return path
# ── Group A — overdue refuses the push ───────────────────────────────────────────────────────
def test_a_overdue_exits_1_and_names_item_and_days():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-341 | 2026-08-19 | fd count + split |\n")
rc, out = run_gate(reg, today="2026-08-20")
check("A: overdue exits 1", rc == 1, "rc=%d" % rc)
check("A: names the item", "R-341" in out)
check("A: names the due date", "2026-08-19" in out)
check("A: says how many days overdue", "1 day(s) OVERDUE" in out)
check("A: points at the R-row for the command", "R-341 row of" in out)
# ── Group B — a future check is silent but not mute ──────────────────────────────────────────
def test_b_future_exits_0_and_still_reports_what_it_saw():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-341 | 2026-08-25 | fd count + split |\n")
rc, out = run_gate(reg, today="2026-08-20")
check("B: future exits 0", rc == 0, "rc=%d" % rc)
check("B: prints the pending count", "1 dated check(s) pending" in out)
check("B: prints the nearest due date", "2026-08-25" in out)
check("B: a passing run is not mute", "nearest:" in out)
# ── Group C — due exactly today counts as due ────────────────────────────────────────────────
def test_c_due_today_is_due():
"""The boundary is `due <= today`. Assert the CONVICTION, not merely a non-zero exit.
The first version of this test asserted only `rc == 1`, and the red-proof caught it being
hollow: flipping `<=` to `<` sent the row into neither bucket, `min()` raised on an empty
list, and the traceback exited 1 — so the test passed while the boundary was wrong. An exit
code alone cannot distinguish a verdict from a crash, which is why the banner is asserted and
a traceback is explicitly ruled out.
"""
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-341 | 2026-08-19 | fd count + split |\n")
rc, out = run_gate(reg, today="2026-08-19")
check("C: due TODAY exits 1 (boundary is <=)", rc == 1, "rc=%d" % rc)
check("C: exits 1 as a VERDICT, not a traceback", "DUE-CHECKS GATE FAILED" in out)
check("C: did not crash", "Traceback" not in out)
check("C: says DUE TODAY rather than overdue", "DUE TODAY" in out)
def test_c_gate_never_ends_in_a_traceback():
"""Whatever the input, the gate exits 0/1/2 with a message — never a Python traceback."""
cases = {
"overdue": "| R-341 | 2026-08-19 | x |\n",
"future": "| R-341 | 2099-01-01 | x |\n",
"empty": "",
}
with tempfile.TemporaryDirectory() as tmp:
for name, rows in cases.items():
reg = make_register(tmp, HEADER + rows, name="OPEN-%s.md" % name)
rc, out = run_gate(reg, today="2026-08-20")
check("C: %s ends in a verdict not a traceback" % name,
"Traceback" not in out and rc in (0, 1, 2), "rc=%d" % rc)
# ── Group D — an orphaned row ────────────────────────────────────────────────────────────────
def test_d_orphaned_item_is_a_conviction():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-999 | 2026-09-30 | something |\n",
item_rows=("R-341",))
rc, out = run_gate(reg, today="2026-08-20")
check("D: orphan exits non-zero", rc != 0, "rc=%d" % rc)
check("D: names the orphaned item", "R-999" in out)
check("D: names the broken coupling", "no row in the register" in out
or "no `| **R-999** |` row found" in out)
def test_d_orphan_convicts_even_when_date_is_far_away():
"""The breakage is the missing row, not the timing — so a far-future orphan still convicts."""
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-888 | 2099-01-01 | far future |\n")
rc, out = run_gate(reg, today="2026-08-20")
check("D: far-future orphan still convicts", rc != 0, "rc=%d" % rc)
# ── Group E — malformed or missing block is INCONCLUSIVE, never a pass ───────────────────────
def test_e_missing_block_exits_2():
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "OPEN-ITEMS.md")
with open(path, "w", encoding="utf-8") as fh:
fh.write("# OPEN-ITEMS (fixture)\n\n| **R-341** | d | s | — | a | CC |\n")
rc, out = run_gate(path, today="2026-08-20")
check("E: missing block exits 2 (NOT 0)", rc == 2, "rc=%d" % rc)
check("E: says inconclusive", "INCONCLUSIVE" in out)
check("E: explains why 0 would be wrong", "inert-seam" in out or "not a pass" in out)
def test_e_missing_register_exits_2():
with tempfile.TemporaryDirectory() as tmp:
rc, out = run_gate(os.path.join(tmp, "nope.md"), today="2026-08-20")
check("E: absent register exits 2", rc == 2, "rc=%d" % rc)
def test_e_malformed_row_exits_2_naming_the_line():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-341 | not-a-date | fd count |\n")
rc, out = run_gate(reg, today="2026-08-20")
check("E: bad date exits 2", rc == 2, "rc=%d" % rc)
check("E: names the offending value", "not-a-date" in out)
check("E: names the exact line", "line " in out)
def test_e_non_row_prose_in_block_exits_2_naming_the_line():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "this is prose, not a row\n")
rc, out = run_gate(reg, today="2026-08-20")
check("E: prose inside the block exits 2", rc == 2, "rc=%d" % rc)
check("E: quotes the offending line", "this is prose" in out)
def test_e_duplicated_block_exits_2():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-341 | 2026-08-25 | fd |\n", markers=2)
rc, out = run_gate(reg, today="2026-08-20")
check("E: duplicated block exits 2", rc == 2, "rc=%d" % rc)
check("E: says two sources of truth", "more than once" in out)
# ── Group F — a well-formed empty block is a PASS, distinguishable from E ────────────────────
def test_f_empty_block_exits_0_with_its_own_message():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER)
rc, out = run_gate(reg, today="2026-08-20")
check("F: empty block exits 0", rc == 0, "rc=%d" % rc)
check("F: says no dated checks pending", "no dated checks pending" in out)
check("F: distinguishable from a missing block", "INCONCLUSIVE" not in out)
# ── Group G — registration, proven by DRIVING the runner (never a source grep) ────────────────
def test_g_gate_is_registered_in_the_runner():
"""A gate that passes its own tests and is not wired into repo_gates.py is inert.
This runs the runner and looks for the gate's label in its OUTPUT. A grep over
repo_gates.py's source would also match a commented-out line, which is precisely the
failure mode being guarded against.
"""
p = subprocess.run([sys.executable, RUNNER, "--fast"],
capture_output=True, text=True, cwd=ROOT)
out = p.stdout + p.stderr
check("G: runner names the due-checks gate in its output", "due-checks" in out)
check("G: runner reports a verdict line for it",
any(l.strip().startswith("due-checks") and ("OK" in l or "FAILED" in l
or "INCONCLUSIVE" in l)
for l in out.splitlines()))
check("G: runner invoked the gate script by name",
"due_checks_gate.py" in out)
# ── Part 6 — the today-override must ANNOUNCE itself ─────────────────────────────────────────
def test_override_announces_itself():
"""A shell that still has FELHOM_GATE_TODAY exported must not be able to fake a run in silence."""
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-341 | 2099-01-01 | far future |\n")
rc, out = run_gate(reg, today="2026-08-20")
check("P6: valid override is announced", "FELHOM_GATE_TODAY=2026-08-20" in out and "IS SET" in out)
check("P6: the announcement says the real date is being ignored",
"NOT" in out and "real UTC date" in out)
check("P6: announcing does not change the verdict", rc == 0, "rc=%d" % rc)
def test_malformed_override_is_inconclusive_not_a_silent_fallback():
with tempfile.TemporaryDirectory() as tmp:
reg = make_register(tmp, HEADER + "| R-341 | 2099-01-01 | far future |\n")
rc, out = run_gate(reg, today="not-a-date")
check("P6: malformed override exits 2", rc == 2, "rc=%d" % rc)
check("P6: names the variable and the bad value", "FELHOM_GATE_TODAY" in out and "not-a-date" in out)
def main():
print("test_due_checks_gate")
for fn in sorted((v for k, v in globals().items() if k.startswith("test_")),
key=lambda f: f.__name__):
fn()
print("")
print("passed: %d failed: %d" % (len(PASSED), len(FAILED)))
if FAILED:
print("")
for f in FAILED:
print(" FAILED: %s" % f)
return 1 if FAILED else 0
if __name__ == "__main__":
sys.exit(main())