104ef34f57
gates / gates (push) Successful in 15s
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).
251 lines
12 KiB
Python
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())
|