681cc663ef
gates / gates (push) Failing after 13s
Every gate was DECOYED - the label constructed without the fact, the gate run, the verdict recorded.
No verdict here was reached by reading, because reading is exactly how the five prior instances hid.
SCOPE IS A FACT TOO, and it was the big one. Six gates decided what to look at with os.listdir - one
directory level. Every one was green AND CORRECT, because no template subdirectory exists today; every
one would have gone blind the moment anyone added templates/partials/, which is an ordinary act. A
single planted file carrying an emoji, a native confirm(), hand-rolled row markup, a dangling JS id
reference, a templated secret and an unregistered retrieval promise passed all six.
THE CONTROL IS WHAT MAKES THAT A MEASUREMENT: mojibake and docker-v already used os.walk, saw the
identical planted file, and convicted. So the cause was the listing, not the decoy.
COMMENTS ARE NOT CODE, AND COMMENTS ARE NOT CONTROLS. debug-routes matched `case subpath == "x"` in
raw text, so a case left in a commented-out block counted as a live handler - which is R-400's
original defect (seven dead controls on the page an operator opens when something is already wrong)
reached through the one door its own gate could not see. app-row-dedup's MUST_USE check had the same
shape: a commented-out {{template "app_list_row"}} satisfied it.
Stripping is deliberately crude in debug_route_gate, and that is correct there: its own docstring
insists on ten lines that cannot rot. A // inside a string literal truncates that line, which can
only ever HIDE a reference, never invent one - it fails in the safe direction.
NOT FIXED, and left open with its decoy rather than quietly patched: R-425, offbox-rename scans a
fixed three-entry FILES list, so banned NAS branding in a NEW offbox template passes. The scope was
correct when written and silently narrows every time the feature grows a file.
test_gate_decoys.py holds 10 decoys and declares COVERS, which felhom.eu's new decoy-coverage gate
AST-parses - a substring search for coverage would be the very shape this sweep exists to find.
No Go code. No version bump. No image. No golden owed.
Survey: felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md
161 lines
7.1 KiB
Python
161 lines
7.1 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",
|
|
}
|
|
|
|
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)
|
|
|
|
# --- 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)
|