1af89a54af
Five v0.247.0 fixtures re-captured from UNCONVERTED v0.246.0 (89dd3e94) because their
one-word Hungarian data values had to become multi-word; two new cases (backups_tier_due,
app_info_installable) captured the same way — the coverage test found both gaps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
202 lines
10 KiB
Python
202 lines
10 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)",
|
|
}
|
|
|
|
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}}"'))
|
|
|
|
# --- 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)
|