Files
felhom-controller/controller/scripts/test_gate_decoys.py
T
admin 5270bad76e
gates / gates (push) Successful in 23s
v0.252.0 — the sentences the program builds follow the language (R-557 slice 2 release A)
Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still
Hungarian literals in Go, so an English household clicked an English button and was
answered in Hungarian. 226 of them move into the bundle here.

A flash was the hard part: it travels inside the redirect URL and is rendered by a
DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by
an older controller carries prose and is shown verbatim — never a raw key, never dropped.

Also converted: page data and view-model text, the internal/api JSON answers, the alert
banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at
display, and the four page titles built around an app name (R-566 closed).

Hungarian is byte-identical, and that is measured rather than read:
scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses
a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict.
Its own first version filtered the capture through an ASCII-Hungarian word list and missed
seven real literals — the R-565 class. The filter is gone.

Nothing on the wire moved, and wire goldens now hold it there: the report's health
warnings and every notify event message stay Hungarian, because the hub MAILS the
controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those.

MinAgent: 0.131.0 (unchanged). No hub release needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 10:15:56 +02:00

223 lines
11 KiB
Python

#!/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)",
}
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"<p>Kesz \U0001F600</p>\n")
in_subdir("native-confirm", "native_confirm_gate.py",
u"<button onclick=\"confirm('biztos?')\">x</button>\n")
in_subdir("app-row-dedup", "app_row_dedup_gate.py", u'<div class="app-row ">hand-rolled</div>\n')
in_subdir("template-id", "template_id_gate.py",
u'<div id="realOne"></div>\n<script>document.getElementById("noSuchId").x=1;</script>\n')
in_subdir("secret-markup", "secret_in_markup_gate.py",
u'<input type="password" value="{{ .RetrievalPassword }}">\n')
in_subdir("retrieval-promise", "retrieval_promise_gate.py",
u"<p>A jelszavat barmikor visszaallithatja innen.</p>\n"
u"<p>Bovebben: visszaállítható a kóddal.</p>\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"<p>árvíztuquotrő</p>\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'<!-- was: \1 -->', 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")
swapped("secret-markup/bundle", "secret_in_markup_gate.py", EN_JSON,
_one('"launcher.link_masolasa": "Copy link"', '"launcher.link_masolasa": "Copy {{.RetrievalPassword}}"'))
# --- 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", ""))
# --- 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)