#!/usr/bin/env python3 # -*- coding: utf-8 -*- """test_gate_decoys.py — can this gate be fooled by a LABEL? (R-421) The controller half of the decoy sweep. Rationale, and the four failure shapes it hunts, are in `felhom.eu/scripts/test_gate_decoys.py` and `documentation/audits/AUDIT-gate-decoys-2026-09-01.md`. TWO HOLES THIS FILE PINS, both measured on 2026-09-01 and both fixed the same day: * **Six gates decided their SCOPE with `os.listdir`**, one directory level. No template subdirectory existed, so every one was green and correct — and would have stayed green the moment anyone added `templates/partials/`, which is an ordinary act. `mojibake` and `docker-v` already used `os.walk` and caught the same planted file, which is the control that proved the cause was the listing and not the decoy. * **`debug-routes` and `app-row-dedup` matched text inside COMMENTS.** A dispatcher case left in a commented-out block counted as a live handler — which is R-400's original defect reached through the one door its own gate could not see. Run from `controller/`: python3 scripts/test_gate_decoys.py Exit 0 all decoys rejected · 1 a decoy passed. """ import io import os import re import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) CTRL = os.path.dirname(HERE) TPL = os.path.join(CTRL, "internal", "web", "templates") SUB = os.path.join(TPL, "partials") # ── WHAT THIS FILE COVERS ──────────────────────────────────────────────────────────────────────── # AST-parsed by felhom.eu/scripts/decoy_coverage_gate.py. See that file for why it is a declaration # and not a grep. COVERS = { "emoji": "an emoji in templates/partials/ (scope was os.listdir)", "native-confirm": "a native confirm() in templates/partials/", "app-row-dedup": "hand-rolled row markup in partials/, AND a commented-out partial call", "template-id": "a JS reference to a missing id, in partials/", "secret-markup": "a secret templated into markup, in partials/", "retrieval-promise": "an unregistered retrieval promise, in partials/", "mojibake": "CONTROL: already walked; proves the planted file is really reachable", "debug-routes": "a live dispatcher case commented out - the button survives, the handler dies", "golden-notice": "R-410 in the other direction: an empty dir must not count as a bake", "minagent-header": "the word MinAgent in prose / a code span, not the header line (test_minagent_header_gate.py)", "i18n": "an undefined marker key, a pleading English value, a shrinking/growing gap (v0.247.0)", "go-parity": "a Go-side key REWORDED, a key citing text no base literal has, and a converted " "key left out of the map (v0.252.0, R-557); a call that lost an argument, and a key glued " "to more text with + (R-576)", "gofmt": "R-454: a planted unformatted .go file in internal/ is convicted; the clean tree passes", "offbox-rename": "R-425: NAS branding in a NEW backups*.html, and in a bundle value an offbox Go file " "names; control: the same token in the network-storage feature's copy is accepted", } fails = [] ran = 0 def gate(script): p = subprocess.run([sys.executable, os.path.join("scripts", script)], cwd=CTRL, capture_output=True, text=True) return p.returncode, p.stdout + p.stderr def in_subdir(name, script, content): """Plant a template one directory down and assert the gate still sees it.""" global ran ran += 1 made = not os.path.isdir(SUB) if made: os.makedirs(SUB) f = os.path.join(SUB, "decoy.html") io.open(f, "w", encoding="utf-8").write(content) try: rc, out = gate(script) finally: os.remove(f) if made and os.path.isdir(SUB) and not os.listdir(SUB): os.rmdir(SUB) if rc == 0: fails.append("%s: a planted template in templates/partials/ PASSED — the gate's scope is a " "directory listing, not the set of templates (R-421)\n%s" % (name, out[-400:])) else: print(" ok %-20s sees templates at any depth" % name) def swapped(name, script, path, transform, expect="convict"): global ran ran += 1 b = io.open(path, encoding="utf-8").read() try: io.open(path, "w", encoding="utf-8").write(transform(b)) rc, out = gate(script) finally: io.open(path, "w", encoding="utf-8").write(b) if (rc != 0) != (expect == "convict"): fails.append("%s: rc=%d, expected %s\n%s" % (name, rc, expect, out[-400:])) else: print(" ok %-20s %s" % (name, "decoy rejected" if expect == "convict" else "genuine accepted")) print("decoys — felhom-controller") # --- SCOPE: the six listdir gates, each with content that actually triggers it ----------------- in_subdir("emoji", "emoji_gate.py", u"
Kesz \U0001F600
\n") in_subdir("native-confirm", "native_confirm_gate.py", u"\n") in_subdir("app-row-dedup", "app_row_dedup_gate.py", u'A jelszavat barmikor visszaallithatja innen.
\n" u"Bovebben: visszaállítható a kóddal.
\n") # --- CONTROL: two gates already walked. If these ever fail, the decoy is wrong, not the gate ---- in_subdir("mojibake (CONTROL)", "mojibake_gate.py", u"árvÃztuquotrÅ‘
\n") # --- COMMENTS ARE NOT CODE (R-421) ------------------------------------------------------------- DISPATCH = os.path.join(CTRL, "internal", "web", "handler_debug.go") DEBUG_TPL = os.path.join(CTRL, "internal", "web", "templates", "debug.html") def _comment_out_a_real_case(src): """Take a LIVE dispatcher case and comment it out. The button stays; the handler dies.""" m = re.search(r'^(\s*)(case subpath == "[A-Za-z0-9/_-]+".*:)$', src, re.M) assert m, "no dispatcher case found — the decoy cannot be built" return src[:m.start()] + m.group(1) + "// " + m.group(2) + src[m.end():] swapped("debug-routes/comment", "debug_route_gate.py", DISPATCH, _comment_out_a_real_case) def _comment_out_the_partial(src): return re.sub(r'(\{\{template "app_list_row".*?\}\})', r'', src) swapped("app-row-dedup/comment", "app_row_dedup_gate.py", os.path.join(TPL, "dashboard.html"), _comment_out_the_partial) # --- i18n (v0.247.0): copy moved OUT of the templates into the bundles. Two families of decoy: --- # --- the new gate itself, and every copy gate that must still see copy that now lives there. --- LOC = os.path.join(CTRL, "internal", "i18n", "locales") EN_JSON, HU_JSON = os.path.join(LOC, "en.json"), os.path.join(LOC, "hu.json") LAUNCHER = os.path.join(TPL, "launcher.html") def _one(old, new): def t(src): assert src.count(old) == 1, "decoy anchor %r not found exactly once — the decoy cannot be built" % old return src.replace(old, new) return t swapped("i18n/undefined-key", "i18n_missing_gate.py", LAUNCHER, _one('{{T "launcher.link_masolasa"}}', '{{T "launcher.no_such_key"}}')) swapped("i18n/pleading", "i18n_missing_gate.py", EN_JSON, _one('"launcher.link_masolasa": "Copy link"', '"launcher.link_masolasa": "Please copy the link"')) swapped("i18n/gap-grew", "i18n_missing_gate.py", EN_JSON, _one(' "launcher.link_masolasa": "Copy link",\n', '')) swapped("i18n/new-formal-form", "i18n_missing_gate.py", HU_JSON, _one('"launcher.link_masolasa": "Link másolása"', '"launcher.link_masolasa": "Kattintson a link másolásához"')) # Scope: the copy gates read the page AS RENDERED, so copy that lives in a bundle is still judged. swapped("emoji/bundle", "emoji_gate.py", EN_JSON, _one('"launcher.link_masolasa": "Copy link"', '"launcher.link_masolasa": "Copy link \U0001F600"')) swapped("native-confirm/bundle", "native_confirm_gate.py", EN_JSON, _one('"launcher.masolva": "Copied"', '"launcher.masolva": "Copied\u0027+confirm(\u0027x\u0027)+\u0027"')) swapped("retrieval-promise/bundle", "retrieval_promise_gate.py", HU_JSON, _one('"launcher.link_masolasa": "Link másolása"', '"launcher.link_masolasa": "A mentésed bármikor visszaállítható."')) # Slice 1 (R-556): an English retrieval PROMISE is judged like a Hungarian one — and the same place # carrying a sentence that promises nothing is accepted (the gate registers claims, not words). swapped("retrieval-promise/en", "retrieval_promise_gate.py", EN_JSON, _one('"launcher.link_masolasa": "Copy link"', '"launcher.link_masolasa": "Your old backups can be restored at any time."')) swapped("retrieval-promise/en-ok", "retrieval_promise_gate.py", EN_JSON, _one('"launcher.link_masolasa": "Copy link"', '"launcher.link_masolasa": "Your old backups are listed on the restore page."'), expect="accept") # R-564: the Hungarian SPLIT verb („állíthatók vissza") is the same claim as the joined stem; a planted # split-verb promise must convict, and a plain „Vissza" (a back link, no claim) must not. swapped("retrieval-promise/split-verb", "retrieval_promise_gate.py", HU_JSON, _one('"launcher.link_masolasa": "Link másolása"', '"launcher.link_masolasa": "A régi mentéseid a kóddal bármikor állíthatók vissza."')) swapped("retrieval-promise/split-ok", "retrieval_promise_gate.py", HU_JSON, _one('"launcher.link_masolasa": "Link másolása"', '"launcher.link_masolasa": "Vissza a listához"'), expect="accept") swapped("secret-markup/bundle", "secret_in_markup_gate.py", EN_JSON, _one('"launcher.link_masolasa": "Copy link"', '"launcher.link_masolasa": "Copy {{.RetrievalPassword}}"')) # --- offbox-rename (R-425): the scope is discovered by pattern, and bundle copy is judged. --------- ran += 1 _extra = os.path.join(TPL, "backups_offbox_extra.html") io.open(_extra, "w", encoding="utf-8").write(u"A NAS-mentés beállítása
\n") try: _rc, _out = gate("offbox_rename_gate.py") finally: os.remove(_extra) if _rc == 0: fails.append("offbox-rename/new-file: NAS branding in a NEW backups*.html PASSED — the scope is a " "fixed list again (R-425)\n%s" % _out[-400:]) else: print(" ok %-20s decoy rejected" % "offbox-rename/new-file") swapped("offbox-rename/bundle", "offbox_rename_gate.py", HU_JSON, _one('"flash.offbox.run_started": "', '"flash.offbox.run_started": "Mentés a NAS-ra: ')) swapped("offbox-rename/other-feature-ok", "offbox_rename_gate.py", HU_JSON, _one('"storage_network.mit_kell_beallitani_a_nas": "Mit kell beállítani a NAS-on?"', '"storage_network.mit_kell_beallitani_a_nas": "Mit kell beállítani a NAS-on? Ez a NAS-ra vonatkozik."'), expect="accept") # --- gofmt (R-454): an unformatted Go file anywhere under internal/ is convicted. ---------------- ran += 1 _gf = os.path.join(CTRL, "internal", "zz_gofmt_decoy_tmp.go") io.open(_gf, "w", encoding="utf-8").write(u"package internal\nfunc decoy( ) { }\n") try: _rc, _out = gate("gofmt_gate.py") finally: os.remove(_gf) if "NOT CHECKED in CI" in _out: print(" -- %-20s not runnable here (CI, no Go toolchain) — the two cases below pin that mode" % "gofmt") elif _rc != 1 or "zz_gofmt_decoy_tmp.go" not in _out: fails.append("gofmt: an unformatted planted file was not convicted (rc=%d)\n%s" % (_rc, _out[-400:])) else: print(" ok %-20s decoy rejected" % "gofmt") # The gate's two no-Go modes (2026-10-05): with no gofmt reachable, CI passes OUT LOUD and a dev machine is # INCONCLUSIVE. PATH is emptied of Go for both; only the CI marker differs. import tempfile as _tf _EMPTY_PATH_DIR = _tf.mkdtemp(prefix="nogo-") def _gofmt_without_go(ci): env = {k: v for k, v in os.environ.items() if k not in ("GITEA_ACTIONS", "GITHUB_ACTIONS", "GOROOT")} env["PATH"] = _EMPTY_PATH_DIR # nothing on it: sys.executable is called by its full path if ci: env["GITEA_ACTIONS"] = "true" p = subprocess.run([sys.executable, os.path.join("scripts", "gofmt_gate.py")], cwd=CTRL, env=env, capture_output=True, text=True) return p.returncode, p.stdout + p.stderr for _ci, _want_rc, _want_txt, _name in ((True, 0, "NOT CHECKED in CI", "gofmt/ci-without-go"), (False, 2, "INCONCLUSIVE", "gofmt/dev-without-go")): ran += 1 _rc, _out = _gofmt_without_go(_ci) if _rc != _want_rc or _want_txt not in _out: fails.append("%s: want rc=%d and %r, got rc=%d\n%s" % (_name, _want_rc, _want_txt, _rc, _out[-300:])) else: print(" ok %-20s %s" % (_name, "passes out loud" if _ci else "stays undetermined")) # --- go-parity (v0.252.0, R-557): a Go-side message key may only carry base-commit text. --- # --- Three shapes, because the gate makes three different claims. --- GO_KEYS = os.path.join(HERE, "i18n_go_keys.json") _A_KEY = '"flash.share.enabled": "A megosztás bekapcsolva."' _A_HU = '"flash.share.enabled": "A megosztás bekapcsolva."' # 1. name-for-fact: the KEY is still listed and still named in Go, and its text was reworded by one # word. A gate that only checked "is this key accounted for" passes this. swapped("go-parity/reworded", "i18n_go_parity.py", HU_JSON, _one(_A_HU, '"flash.share.enabled": "A megosztás most bekapcsolva."')) # 2. constant-for-measurement: the key map cites text nobody ever wrote. A gate that compared # hu.json against the MAP alone (rather than against the frozen capture) passes this. swapped("go-parity/invented", "i18n_go_parity.py", GO_KEYS, _one(_A_KEY, '"flash.share.enabled": "Ez a mondat sosem létezett."')) # 3. declaration-for-reachability: a converted key is NAMED by Go and quietly dropped from the map. # A gate that only walked the map passes this -- which is how a conversion escapes review. swapped("go-parity/unlisted", "i18n_go_parity.py", GO_KEYS, _one(_A_KEY + ",\n", "")) # 4-5. R-576 -- the CALL lost text while every surviving fragment stayed real (2026-09-18, 7 # producers, this gate green). Built on a real producer: the update-already-running refusal. UPDATE_GO = os.path.join(CTRL, "internal", "stacks", "update.go") _R576 = 'util.MsgError("update.refusal.already", name), "lost the race' # the argument dropped -- the key's text is still byte-equal, only the call is short. swapped("go-parity/lost-argument", "i18n_go_parity.py", UPDATE_GO, _one(_R576, 'util.MsgError("update.refusal.already"), "lost the race')) # the key glued to a continuation with + -- the converter's exact shape. swapped("go-parity/key-concat", "i18n_go_parity.py", UPDATE_GO, _one(_R576, 'util.MsgError("update.refusal.already" + suffix, name), "lost the race')) # CONTROL: one argument that itself holds parens and commas is still ONE argument. swapped("go-parity/nested-arg-ok", "i18n_go_parity.py", UPDATE_GO, _one(_R576, 'util.MsgError("update.refusal.already", fmt.Sprintf("%s,%s", f(a, b), c)), "lost the race'), expect="accept") # --- golden-notice: R-410's decoy, in the other direction. It is ADVISORY, so rc is never the --- # --- question — what it COUNTED is. --- ran += 1 EV = os.path.join(os.path.dirname(os.path.dirname(CTRL)), "felhom.eu", "documentation", "tests", "golden-9.9.9-2026-01-01") if os.path.isdir(os.path.dirname(EV)): os.makedirs(EV) try: p = subprocess.run([sys.executable, os.path.join("scripts", "golden_notice.py"), os.path.dirname(CTRL)], cwd=CTRL, capture_output=True, text=True) out = p.stdout + p.stderr finally: os.rmdir(EV) if "9.9.9" in out and "NOT counted" not in out: fails.append("golden-notice: an EMPTY directory was counted as a bake (R-410 regressed)") else: print(" ok %-20s empty dir not counted as a bake" % "golden-notice") else: print(" -- %-20s SKIPPED: no felhom.eu sibling clone" % "golden-notice") print() if fails: for f in fails: print("FAIL: %s" % f) sys.exit(1) print("all %d controller decoys behaved — labels do not satisfy these gates" % ran)