Files
felhom.eu/scripts/test_guide_quote_gate.py
admin e02bc03819
gates / gates (push) Successful in 24s
hub v0.119.0 — English households get English words for their codes (R-597); R-596/R-598 closed
The setup code and the owner passphrase now follow the household's language,
one word longer in English so the entropy never drops (setup 3 hu / 4 en,
passphrase 5 hu / 6 en). List and count are chosen together so a caller cannot
pair an English list with a Hungarian count. Hungarian is byte-unchanged.

Three claims in the row were wrong and are recorded as such:
  - the RECOVERY CODE is minted by felhom-agent from the EFF list and has
    always been English; the hub does not own it and no row was added.
  - no claim mail states a word count; the only count wording was the bind
    page's passphrase hint, whose English half is now count-free.
  - the proposed phone-safe filter removes 68% of the list (5270 of 7772
    words) and was measured, then declined, with the reason in source.

Also: guide_quote_gate binds the English volunteer guide's three quoted
messages to the controller's English bundle — nothing did, so the guide would
have gone on quoting Hungarian after the fix. Seven decoys, all convicting,
including the name-for-fact one.

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

179 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Decoys for guide_quote_gate (R-421). Run: python3 scripts/test_guide_quote_gate.py
A decoy is the LABEL without the FACT. The shape this gate is most at risk of is
**name-for-fact**: matching the KEY NAME where the fact is the key's VALUE. A guide that lists
`claim.msg.bad_code` in a table of "messages covered" would satisfy a name-matching gate and tell a
tester nothing, because a tester reads sentences, not keys.
The second shape checked here is **declaration-for-reachability** in its sibling form: a gate that
reports OK when it could not read the bundle at all. Scope is a fact -- an absent controller clone
must be INCONCLUSIVE (2), never a pass.
Each case runs the real gate against a built tree and asserts the exit code.
"""
from __future__ import annotations
import io
import json
import os
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
WORKSPACE = os.path.dirname(REPO)
REAL_BUNDLE = os.path.join(
WORKSPACE, "felhom-controller", "controller", "internal", "i18n", "locales", "en.json"
)
GATE_SRC = os.path.join(HERE, "guide_quote_gate.py")
def build(tmp, guide_text, bundle=None, with_bundle=True):
"""Lay out a fake workspace: <tmp>/felhom.eu/{scripts,documentation/...} + the sibling clone."""
repo = os.path.join(tmp, "felhom.eu")
scripts = os.path.join(repo, "scripts")
runbooks = os.path.join(repo, "documentation", "runbooks")
os.makedirs(scripts)
os.makedirs(runbooks)
shutil.copy(GATE_SRC, os.path.join(scripts, "guide_quote_gate.py"))
io.open(os.path.join(runbooks, "VOLUNTEER-first-hour.en.md"), "w", encoding="utf-8").write(guide_text)
if with_bundle:
locales = os.path.join(tmp, "felhom-controller", "controller", "internal", "i18n", "locales")
os.makedirs(locales)
if bundle is None:
bundle = json.load(io.open(REAL_BUNDLE, encoding="utf-8"))
io.open(os.path.join(locales, "en.json"), "w", encoding="utf-8").write(
json.dumps(bundle, ensure_ascii=False, indent=2)
)
return os.path.join(scripts, "guide_quote_gate.py")
def run(gate):
p = subprocess.run([sys.executable, gate], capture_output=True, text=True)
return p.returncode, p.stdout + p.stderr
def real_bundle():
return json.load(io.open(REAL_BUNDLE, encoding="utf-8"))
def honest_guide(b):
return (
"# guide\n\n"
"A wrong code answers **\"%s\"**, and after five tries **\"%s\"**.\n"
"A short password answers **\"%s\"**.\n\n> %s\n"
% (
b["claim.msg.bad_code"],
b["claim.msg.too_many"],
b["claim.msg.password_too_short"] % 12,
b["backup.target.degraded"],
)
)
CASES = []
def case(name):
def deco(fn):
CASES.append((name, fn))
return fn
return deco
@case("control: an honest guide passes")
def _control(tmp):
gate = build(tmp, honest_guide(real_bundle()))
rc, out = run(gate)
return rc == 0, "rc=%d\n%s" % (rc, out)
@case("DECOY name-for-fact: the guide NAMES every key and quotes none")
def _names(tmp):
text = (
"# guide\n\nMessages covered by this guide:\n\n"
"| key | section |\n|---|---|\n"
"| claim.msg.bad_code | 13 |\n"
"| claim.msg.too_many | 13 |\n"
"| claim.msg.password_too_short | 13 |\n"
"| backup.target.degraded | 9 |\n\n"
"All four messages are shown in English.\n"
)
gate = build(tmp, text)
rc, out = run(gate)
return rc == 1 and "NOT QUOTED" in out, "rc=%d\n%s" % (rc, out)
@case("DECOY substring: the guide quotes a PREFIX of the real sentence")
def _prefix(tmp):
b = real_bundle()
text = honest_guide(b).replace(b["backup.target.degraded"], b["backup.target.degraded"][:40])
gate = build(tmp, text)
rc, out = run(gate)
return rc == 1 and "backup.target.degraded" in out, "rc=%d\n%s" % (rc, out)
@case("DECOY the OLD Hungarian quote: the drill-era guide must convict")
def _hungarian(tmp):
b = real_bundle()
text = honest_guide(b).replace(b["claim.msg.bad_code"], "Hibás vagy lejárt kód")
gate = build(tmp, text)
rc, out = run(gate)
return rc == 1 and "claim.msg.bad_code" in out, "rc=%d\n%s" % (rc, out)
@case("DECOY reworded screen: the bundle changes and the guide does not")
def _reworded(tmp):
b = real_bundle()
text = honest_guide(b)
b2 = dict(b)
b2["claim.msg.too_many"] = "Too many attempts - try again later."
gate = build(tmp, text, bundle=b2)
rc, out = run(gate)
return rc == 1 and "claim.msg.too_many" in out, "rc=%d\n%s" % (rc, out)
@case("SCOPE: no controller clone is INCONCLUSIVE (2), never a pass")
def _noclone(tmp):
gate = build(tmp, honest_guide(real_bundle()), with_bundle=False)
rc, out = run(gate)
return rc == 2 and "INCONCLUSIVE" in out, "rc=%d\n%s" % (rc, out)
@case("SCOPE: a key deleted from the bundle convicts, it does not vanish")
def _deleted(tmp):
b = real_bundle()
text = honest_guide(b)
b2 = dict(b)
del b2["claim.msg.bad_code"]
gate = build(tmp, text, bundle=b2)
rc, out = run(gate)
return rc == 1 and "MISSING KEY" in out, "rc=%d\n%s" % (rc, out)
def main():
if not os.path.exists(REAL_BUNDLE):
print("SKIP: the felhom-controller clone is not beside this one", file=sys.stderr)
return 2
failed = 0
for name, fn in CASES:
tmp = tempfile.mkdtemp(prefix="gqg-")
try:
ok, detail = fn(tmp)
finally:
shutil.rmtree(tmp, ignore_errors=True)
print((" PASS " if ok else " FAIL ") + name)
if not ok:
failed += 1
print(" " + detail.replace("\n", "\n "))
print("\nguide-quote decoys: %d/%d" % (len(CASES) - failed, len(CASES)))
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())