Files
felhom.eu/scripts/test_status_view_gate.py
T

194 lines
11 KiB
Python

# -*- coding: utf-8 -*-
"""Decoy tests for status_view_gate.py (R-1174).
Run: python3 scripts/test_status_view_gate.py Exit 0 every case behaved · 1 a case did not.
A DECOY IS THE LABEL WITHOUT THE FACT (R-421). Each case below takes a GENUINE page — the fixture,
shaped like the real STATUS.md — and breaks ONE check the way a real session would: the „Updated"
paragraph with an „Earlier" chain behind it, a decided row left in the decide table, a Hungarian
do-line, a page that meets the size by deleting the P1 or the „Later" line. Each must convict, and
must convict ON THAT CHECK (its `[check]` tag in the output): a decoy caught by some other check
proves nothing about the check it was built for.
Both directions: the genuine fixture and the REAL STATUS.md against the REAL register must pass.
The date switch is tested on both sides (FELHOM_GATE_TODAY). Fixtures are temp files; nothing here
touches the real tree.
"""
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, "status_view_gate.py")
BEFORE = "2026-10-26" # the last warn-only day
AFTER = "2026-10-27" # SWITCH_DATE: convictions refuse
REGISTER = u"""# OPEN-ITEMS — fixture
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
| **R-10** | Security & access | P1 | **Reused logins.** | **NARROWED** | — | rotate | operator |
| **R-11** | Backup & restore | P3 | **Immich off-site size.** | **READY** | D21 | decide | CC |
| **R-12** | Process & tooling | P4 | **A failed disk.** | **OPEN** | — | pull it | operator |
| **R-13** | Business & legal | P4 | **A lawyer.** | WAITING-ON-OPERATOR | — | send | Viktor |
| **R-14** | Install & onboarding | P3 | **The minimum machine.** | WAITING-ON-OPERATOR | — | approve | operator |
| **R-15** | Box system & updates | P3 | **The scratch box.** | OPEN | — | fix | CC |
"""
PAGE = u"""# STATUS — what works, what's broken, what's next
**Ready for the first real tester? Yes.** The laptop is off.
**Updated 2026-10-20 (day).** Hub 0.162.0 is live (your yes). This page is one screen again. A rehearsal runs tonight.
## What works
- A box installs from nothing (map A1).
## What is broken
- **P1:** reused logins (R-10, yours).
- The scratch box (R-15).
## What we work on
- The rehearsal. CC.
## Waiting on you — decide
| # | Row | Question | Pick (why) | The other option costs | If you do nothing | Blocked |
|---|---|---|---|---|---|---|
| D21 | R-11 | Leave out the AI model files now? | **B now** | A: a new restore path | 12 times the data | — |
| D5 | R-14 | Approve the minimum machine? | **Approve** | Change it: one edit | no hardware page | the hardware page |
## Waiting on you — do
**Still needed? (yes/no)**
1. Look at the Calibre-Web page („Adatok és mentés"). Since 2026-10-16.
**The rest**
- Pull the failed disk (R-12). Since 2026-10-12.
- Rotate the reused logins (R-10, P1). Since 2026-10-09.
- Later (yours, not urgent): R-13.
"""
PASSED, FAILED = [], []
def run(page, register=REGISTER, today=BEFORE, raw_page_path=None, raw_reg_path=None):
with tempfile.TemporaryDirectory() as d:
p = raw_page_path or os.path.join(d, "STATUS.md")
r = raw_reg_path or os.path.join(d, "OPEN-ITEMS.md")
if raw_page_path is None and page is not None:
open(p, "w", encoding="utf-8").write(page)
if raw_reg_path is None and register is not None:
open(r, "w", encoding="utf-8").write(register)
env = dict(os.environ)
env.pop("FELHOM_GATE_TODAY", None)
if today:
env["FELHOM_GATE_TODAY"] = today
pr = subprocess.run([sys.executable, GATE, p, r], capture_output=True, text=True, env=env,
encoding="utf-8", errors="replace")
return pr.returncode, pr.stdout + pr.stderr
def mutate(old, new, page=PAGE):
assert page.count(old) == 1, "fixture anchor not unique: %r" % old
return page.replace(old, new)
def case(name, page, expect_rc, must=None, mustnot=None, **kw):
rc, out = run(page, **kw)
ok = rc == expect_rc and (must is None or must in out) and (mustnot is None or mustnot not in out)
(PASSED if ok else FAILED).append(name)
print("%s %s (rc=%d, want %d%s)" % ("PASS" if ok else "FAIL", name, rc, expect_rc,
", must %r" % must if must else ""))
if not ok:
print(" " + out.replace("\n", "\n ")[:1500])
def decoy(name, page, check):
"""A decoy convicts on its own check: WARNING before the switch (exit 0), refusal from it (exit 1)."""
case(name + " / before the switch: warns", page, 0, must="WARNING [%s]" % check, today=BEFORE)
case(name + " / from the switch: refuses", page, 1, must="CONVICTED [%s]" % check, today=AFTER)
# ── the genuine article, both sides of the switch ────────────────────────────────────────────────
case("genuine fixture passes (before)", PAGE, 0, must="status-view: OK", mustnot="WARNING", today=BEFORE)
case("genuine fixture passes (after)", PAGE, 0, must="status-view: OK", today=AFTER)
case("clean run says what it looked at", PAGE, 0, must="lines /", today=BEFORE)
# ── size ─────────────────────────────────────────────────────────────────────────────────────────
decoy("size: 50 more What-works lines", mutate("- A box installs from nothing (map A1).\n",
"- A box installs from nothing (map A1).\n" + "- One more capability, in plain words (map B1).\n" * 50), "size")
decoy("size: few lines, too many characters", mutate("- A box installs from nothing (map A1).\n",
"- A box installs from nothing (map A1). " + "It works well and it is proven live. " * 170 + "\n"), "size")
case("size: a long decide table is exempt", mutate(
"| D5 | R-14 |", "| D5 | R-14 | Approve? | **Approve** | one edit | no page | — |\n" * 60 + "| D6 | R-14 |"),
0, must="status-view: OK", today=AFTER)
# ── updated ──────────────────────────────────────────────────────────────────────────────────────
decoy("updated: news prepended, the old paragraph left behind", mutate(
"**Updated 2026-10-20 (day).**", "**Updated 2026-10-21 (day).** New news.\n\n**Updated 2026-10-20 (day).**"), "updated")
decoy("updated: an Earlier chain inside the paragraph", mutate(
"A rehearsal runs tonight.", "A rehearsal runs tonight. **Earlier:** a lot happened."), "updated")
decoy("updated: four sentences", mutate(
"A rehearsal runs tonight.", "A rehearsal runs tonight. The installer is published."), "updated")
decoy("updated: no Updated paragraph at all", mutate(
"**Updated 2026-10-20 (day).** ", ""), "updated")
case("updated: version numbers are not sentence ends", mutate(
"Hub 0.162.0 is live (your yes).", "Hub 0.162.0 and agent 0.161.0 are live (your yes)."),
0, must="status-view: OK", today=AFTER)
# ── english ──────────────────────────────────────────────────────────────────────────────────────
decoy("english: a Hungarian do-line", mutate(
"**The rest**\n", "**The rest**\n- Nézd meg a Calibre-Web oldalát a demo-hp-n.\n"), "english")
case("english: the same text inside a quote passes", mutate(
"**The rest**\n", "**The rest**\n- Read the line „Nézd meg a Calibre-Web oldalát” on demo-hp.\n"),
0, must="status-view: OK", today=AFTER)
# ── open-ids ─────────────────────────────────────────────────────────────────────────────────────
decoy("open-ids: a closed row cited in What works", mutate(
"(map A1).", "(map A1; R-99 closed it)."), "open-ids")
# ── decide ───────────────────────────────────────────────────────────────────────────────────────
decoy("decide: a DECIDED row left in the table", mutate(
"| D5 | R-14 |", "| D20 | R-11 | **DECIDED 2026-10-20: A** — built and live. | done | — | — | — |\n| D5 | R-14 |"),
"decide")
decoy("decide: a row whose Pick cell reads done", mutate(
"| D5 | R-14 | Approve the minimum machine? | **Approve** |", "| D5 | R-14 | Approve the minimum machine? | done |"),
"decide")
# ── p1 ───────────────────────────────────────────────────────────────────────────────────────────
decoy("p1: the page meets its size by deleting the P1 line", mutate(
"- **P1:** reused logins (R-10, yours).\n", ""), "p1")
decoy("p1: the P1 moved to What works", mutate(
"- **P1:** reused logins (R-10, yours).\n", "", page=mutate(
"- A box installs from nothing (map A1).\n", "- A box installs from nothing (map A1). Logins: R-10.\n")), "p1")
# ── operator ─────────────────────────────────────────────────────────────────────────────────────
decoy("operator: the Later line deleted", mutate("- Later (yours, not urgent): R-13.\n", ""), "operator")
decoy("operator: a waiting row named only in What is broken", mutate(
"- Pull the failed disk (R-12). Since 2026-10-12.\n", "", page=mutate(
"- The scratch box (R-15).\n", "- The scratch box (R-15). The failed disk (R-12).\n")), "operator")
# ── inconclusive: never a pass, no warn period ───────────────────────────────────────────────────
case("inconclusive: a page with no sections", u"# STATUS\n\nAll good.\n", 2, must="INCONCLUSIVE", today=BEFORE)
case("inconclusive: a page missing the do section",
PAGE.split("## Waiting on you — do")[0], 2, must="INCONCLUSIVE", today=BEFORE)
case("inconclusive: an empty register", PAGE, 2, must="INCONCLUSIVE", register=u"# nothing here\n", today=BEFORE)
case("inconclusive: no register file", PAGE, 2, must="INCONCLUSIVE",
raw_reg_path=os.path.join(HERE, "no-such-register.md"), today=BEFORE)
# ── the real page against the real register ──────────────────────────────────────────────────────
case("the real STATUS.md passes against the real register", None, 0, must="status-view: OK",
raw_page_path=os.path.join(ROOT, "STATUS.md"),
raw_reg_path=os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md"), today=None)
print("\n%d passed, %d failed" % (len(PASSED), len(FAILED)))
if FAILED:
for f in FAILED:
print(" FAILED: " + f)
sys.exit(1)
sys.exit(0)