# -*- 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("") body.append(rows_block.rstrip("\n")) body.append("") 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())