4c3c2037fd
49 Hungarian values on the debug (RESET prompt), storage, network-storage, security, system, deploy, restore and remote-backup copy moved from the formal („ön") to the product's te-form („írd be", „add meg", „hozz létre", „biztosan eltávolítod", „engedélyezd", „próbáld", ...). i18n_missing_gate.py's stem list widened by 33 stems (+ one listed third-person exception, „sora adja meg"); it counted 61 on the previous bundle and counts 0 now, ceiling stays 0. Decoys: a widened-stem form convicts, its te-form twin and the third-person phrase pass. 38 parity fixtures changed by exactly those bytes. Seven Go-side keys listed as REWORDED in the go-parity map. Pinned by TestR516_WidenedFormalFormsAreGone; two tests that quoted the old words follow them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
501 lines
27 KiB
Python
501 lines
27 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/; R-325: the shared vocabulary "
|
|
"absent (INCONCLUSIVE) and an invented stem in it convicting",
|
|
"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",
|
|
"docker-v": "R-426: an unallowlisted `-v /etc:/x` in a NEW .go file two directories down "
|
|
"(internal/ and cmd/) convicts; controls: the clean tree, the same line in a _test.go",
|
|
"reuse-refs": "R-426: a cited .go and a cited .md path that do not exist, planted in THIS repo's "
|
|
"REUSE.md - vs the real file",
|
|
"instructions": "R-426: a component version literal in THIS repo's CLAUDE.md effective text - vs the "
|
|
"same sentence inside an HTML comment",
|
|
"observations": "R-426: R-419 in THIS repo's REPORT.md - an Observations note SAYING it carries no "
|
|
"marker - vs the two genuine markers",
|
|
}
|
|
|
|
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"'))
|
|
# R-516 (2026-10-06): the widened stems -- a form the original list did not see convicts, its te-form
|
|
# twin and the one listed third-person statement pass.
|
|
swapped("i18n/new-formal-wide", "i18n_missing_gate.py", HU_JSON,
|
|
_one('"launcher.link_masolasa": "Link másolása"', '"launcher.link_masolasa": "Írja be a link nevét"'))
|
|
swapped("i18n/te-form-wide", "i18n_missing_gate.py", HU_JSON,
|
|
_one('"launcher.link_masolasa": "Link másolása"', '"launcher.link_masolasa": "Írd be a link nevét"'),
|
|
expect="accept")
|
|
swapped("i18n/third-person-ok", "i18n_missing_gate.py", HU_JSON,
|
|
_one('"launcher.link_masolasa": "Link másolása"', '"launcher.link_masolasa": "A Megosztás oldal sora adja meg"'),
|
|
expect="accept")
|
|
# 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"<p>A NAS-mentés beállítása</p>\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")
|
|
|
|
# --- retrieval-promise, R-325: the stems are IMPORTED from felhom.eu/scripts/customer_copy_vocab.py. ---
|
|
# Two shapes. (1) The vocabulary absent must be INCONCLUSIVE, never a pass on an empty list.
|
|
# (2) declaration-for-reachability: a gate that still carried a private literal would ignore the
|
|
# shared file. A doctored vocabulary with one invented stem, and a template using only that stem,
|
|
# must convict — proof the shared list is the one in force.
|
|
import shutil # noqa: E402
|
|
import tempfile # noqa: E402
|
|
|
|
|
|
def _retrieval_with_vocab(vocab_src):
|
|
d = tempfile.mkdtemp(prefix="r325-")
|
|
try:
|
|
if vocab_src is not None:
|
|
io.open(os.path.join(d, "customer_copy_vocab.py"), "w", encoding="utf-8").write(vocab_src)
|
|
env = dict(os.environ, FELHOM_SHARED_SCRIPTS=d)
|
|
p = subprocess.run([sys.executable, os.path.join("scripts", "retrieval_promise_gate.py")],
|
|
cwd=CTRL, env=env, capture_output=True, text=True)
|
|
return p.returncode, p.stdout + p.stderr
|
|
finally:
|
|
shutil.rmtree(d, ignore_errors=True)
|
|
|
|
|
|
ran += 1
|
|
_rc, _out = _retrieval_with_vocab(None)
|
|
if _rc != 2 or "INCONCLUSIVE" not in _out:
|
|
fails.append("retrieval-promise/vocab-absent: want rc=2 INCONCLUSIVE, got rc=%d\n%s" % (_rc, _out[-300:]))
|
|
else:
|
|
print(" ok %-20s %s" % ("retrieval-promise/vocab-absent", "stays undetermined"))
|
|
|
|
ran += 1
|
|
_made = not os.path.isdir(SUB)
|
|
if _made:
|
|
os.makedirs(SUB)
|
|
_planted = os.path.join(SUB, "decoy-r325.html")
|
|
io.open(_planted, "w", encoding="utf-8").write(u"<p>Minden adatod felhomdecoyszohato marad.</p>\n")
|
|
try:
|
|
_rc, _out = _retrieval_with_vocab(u'RETRIEVAL_STEMS = ["visszaállíthat", "felhomdecoyszo"]\n')
|
|
finally:
|
|
os.remove(_planted)
|
|
if _made and os.path.isdir(SUB) and not os.listdir(SUB):
|
|
os.rmdir(SUB)
|
|
if _rc != 1 or "felhomdecoyszo" not in _out:
|
|
fails.append("retrieval-promise/vocab-is-live: an invented stem in the SHARED list did not convict — the "
|
|
"gate is not reading customer_copy_vocab.py (rc=%d)\n%s" % (_rc, _out[-300:]))
|
|
else:
|
|
print(" ok %-20s %s" % ("retrieval-promise/vocab-is-live", "decoy rejected"))
|
|
|
|
# --- 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")
|
|
|
|
# --- docker-v (R-426): a NEW file with an unreviewed host-path mount, anywhere under the roots ------
|
|
def _plant_go(rel, body):
|
|
path = os.path.join(CTRL, rel)
|
|
made = []
|
|
d = os.path.dirname(path)
|
|
while not os.path.isdir(d):
|
|
made.append(d)
|
|
d = os.path.dirname(d)
|
|
for m in reversed(made):
|
|
os.mkdir(m)
|
|
io.open(path, "w", encoding="utf-8").write(body)
|
|
return path, made
|
|
|
|
|
|
def _unplant(path, made):
|
|
os.remove(path)
|
|
for m in made:
|
|
os.rmdir(m)
|
|
|
|
|
|
DOCKER_V_LINE = u'package decoy\n\nvar args = []string{"run", "-v", "/etc:/x", "alpine"}\n'
|
|
for _label, _rel, _expect in (
|
|
("docker-v/new-internal-file", os.path.join("internal", "decoydockerv", "deep", "decoy.go"), "convict"),
|
|
("docker-v/new-cmd-file", os.path.join("cmd", "decoydockerv", "decoy.go"), "convict"),
|
|
("docker-v/_test.go (CONTROL)", os.path.join("internal", "decoydockerv", "decoy_test.go"), "accept")):
|
|
ran += 1
|
|
_p, _made = _plant_go(_rel, DOCKER_V_LINE)
|
|
try:
|
|
_rc, _out = gate("docker_run_volume_path_gate.py")
|
|
finally:
|
|
_unplant(_p, _made)
|
|
if (_rc != 0) != (_expect == "convict") or (_expect == "convict" and "/etc:/x" not in _out):
|
|
fails.append("%s: rc=%d, expected %s\n%s" % (_label, _rc, _expect, _out[-400:]))
|
|
else:
|
|
print(" ok %-30s %s" % (_label, "decoy rejected" if _expect == "convict" else "genuine accepted"))
|
|
ran += 1
|
|
_rc, _out = gate("docker_run_volume_path_gate.py")
|
|
if _rc != 0:
|
|
fails.append("docker-v/clean tree (CONTROL): rc=%d\n%s" % (_rc, _out[-400:]))
|
|
else:
|
|
print(" ok %-30s %s" % ("docker-v/clean tree (CONTROL)", "genuine accepted"))
|
|
|
|
|
|
# --- the SHARED felhom.eu gates (R-426), against THIS repo's inputs --------------------------------
|
|
# A scratch WORKSPACE: a clone of this repo, named as the main clone, beside symlinks to the siblings,
|
|
# because the shared scripts reach across (REUSE.md cites felhom.eu paths; instructions_gate reads the
|
|
# workspace CLAUDE.md). The plants go into the clone, never into this tree. Mirrors felhom-agent b78a0ff.
|
|
# DECOY_SHARED_DIR exists for ONE purpose, the red-proof: it lets a mutated COPY of the shared scripts be
|
|
# judged without editing the felhom.eu clone. Unset, the suite judges the real shared scripts.
|
|
import shutil
|
|
import tempfile
|
|
|
|
REPO = os.path.dirname(CTRL)
|
|
PARENT = os.path.dirname(REPO)
|
|
SHARED = os.environ.get("DECOY_SHARED_DIR") or os.path.join(PARENT, "felhom.eu", "scripts")
|
|
|
|
|
|
def _run(argv, cwd):
|
|
p = subprocess.run(argv, cwd=cwd, capture_output=True, text=True, input="")
|
|
return p.returncode, p.stdout + p.stderr
|
|
|
|
|
|
def shared_cases():
|
|
global ran
|
|
for g in ("reuse_refs_check.py", "instructions_gate.py", "observations_gate.py"):
|
|
if not os.path.isfile(os.path.join(SHARED, g)):
|
|
fails.append("shared gate %s is MISSING beside this clone (tried %s) - a failure, never a skip"
|
|
% (g, SHARED))
|
|
return
|
|
ws = tempfile.mkdtemp(prefix="ctrl-decoy-shared-")
|
|
try:
|
|
space = os.path.join(ws, "workspace")
|
|
os.makedirs(space)
|
|
for entry in sorted(os.listdir(PARENT)):
|
|
if entry in ("felhom.eu", "felhom-agent", "app-catalog-felhom.eu", "homelab-manifests",
|
|
"CLAUDE.md", ".claude-memory"):
|
|
os.symlink(os.path.join(PARENT, entry), os.path.join(space, entry))
|
|
repo = os.path.join(space, "felhom-controller")
|
|
rc, out = _run(["git", "clone", "-q", "--no-tags", "file://" + REPO, repo], ws)
|
|
if rc != 0:
|
|
fails.append("shared: could not clone this repo into the scratch workspace\n" + out[-400:])
|
|
return
|
|
# the WORKING-TREE inputs the plants go into, so a case judges today's file
|
|
for f in ("REUSE.md", "CLAUDE.md", "REPORT.md"):
|
|
shutil.copy(os.path.join(REPO, f), os.path.join(repo, f))
|
|
|
|
def case(name, script, relpath, extra, expect_rc, must=()):
|
|
global ran
|
|
ran += 1
|
|
p = os.path.join(repo, relpath)
|
|
backup = io.open(p, encoding="utf-8").read()
|
|
try:
|
|
if extra:
|
|
io.open(p, "w", encoding="utf-8").write(backup + extra)
|
|
rc, out = _run([sys.executable, os.path.join(SHARED, script), repo], repo)
|
|
finally:
|
|
io.open(p, "w", encoding="utf-8").write(backup)
|
|
missing = [m for m in must if m not in out]
|
|
if rc == expect_rc and not missing:
|
|
print(" ok %-62s rc=%d" % (name, rc))
|
|
else:
|
|
hole = " - LIVE HOLE" if expect_rc != 0 and rc == 0 else ""
|
|
fails.append("%s: rc=%d expected %d%s; missing %s\n%s" % (name, rc, expect_rc, hole, missing,
|
|
out[-900:]))
|
|
|
|
case("reuse-refs: GENUINE: this repo's REUSE.md", "reuse_refs_check.py", "REUSE.md", "", 0, ("FAILED 0",))
|
|
case("reuse-refs: FACT: a cited .go path that does not exist", "reuse_refs_check.py", "REUSE.md",
|
|
u"\n- see `controller/internal/web/does_not_exist.go`\n", 1, ("does_not_exist.go",))
|
|
case("reuse-refs: FACT: a cited .md path that does not exist", "reuse_refs_check.py", "REUSE.md",
|
|
u"\n- see `docs/99-does-not-exist.md`\n", 1, ("99-does-not-exist.md",))
|
|
case("instructions: GENUINE: this repo's CLAUDE.md", "instructions_gate.py", "CLAUDE.md", "", 0,
|
|
("instructions_gate: OK",))
|
|
case("instructions: FACT: a version literal in effective text", "instructions_gate.py", "CLAUDE.md",
|
|
u"\nThe controller runs v0.298.0 today.\n", 1, ("v0.298.0",))
|
|
case("instructions: GENUINE: the same sentence in an HTML comment", "instructions_gate.py", "CLAUDE.md",
|
|
u"\n<!--\nThe controller ran v0.298.0 on 2026-10-06.\n-->\n", 0, ("instructions_gate: OK",))
|
|
case("observations: FACT: R-419, prose SAYING it has no marker", "observations_gate.py", "REPORT.md",
|
|
u"\n## Observations\n\n1. **A real finding.** It carries no `FILED:` marker and no "
|
|
u"`NOT-A-FINDING:` marker, deliberately.\n", 1)
|
|
case("observations: GENUINE: a FILED marker", "observations_gate.py", "REPORT.md",
|
|
u"\n## Observations\n\n1. **A real finding.** Something broke. **FILED: R-419**\n", 0)
|
|
case("observations: GENUINE: a NOT-A-FINDING marker", "observations_gate.py", "REPORT.md",
|
|
u"\n## Observations\n\n1. **A real finding.** Odd. **NOT-A-FINDING: my own typo, corrected in "
|
|
u"the same minute.**\n", 0)
|
|
finally:
|
|
shutil.rmtree(ws, ignore_errors=True)
|
|
|
|
|
|
shared_cases()
|
|
|
|
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)
|