#!/usr/bin/env python3 # -*- coding: utf-8 -*- """test_gate_decoys.py — can this gate be fooled by a LABEL? (R-421) WHY THIS FILE EXISTS. Four times in one week a gate turned out to be matching a name instead of the thing it named — R-410 (a `mkdir` turned the release gate green), R-400 (seven debug controls that answered nothing), R-378 (a status word inside a sentence), R-419 (a phrase inside prose, including prose saying the marker was absent). **All four were found by accident.** The gates are the machinery that enforces everything else in this project, and they were the one part nothing checked. A DECOY IS THE LABEL WITHOUT THE FACT. Each test below constructs one, runs the real gate, and asserts it CONVICTS. Where a decoy would pass, that is a live hole. ⚠ A DECOY MUST BE THE SHAPE A REAL SESSION WOULD PRODUCE. R-419 was not found by an absurd input — it was found by a genuine note explaining that it carried no marker. That is the standard. A decoy nobody would ever write proves nothing, and saying so is a result. ⚠ EVERY TEST ASSERTS BOTH DIRECTIONS where it can. A gate that rejects the decoy AND rejects the genuine article is worse than the hole it replaced. Fixtures are planted in the real tree and removed in a `finally`. The suite asserts the tree is unchanged at the end. Run from the repo root: python3 scripts/test_gate_decoys.py Exit 0 all decoys rejected · 1 a decoy passed (a live hole). """ import io import re import json import os import shutil import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # ONE RUN AT A TIME (2026-10-08). The decoys mutate REAL files and restore them from a backup taken at plant time. Two # runs at once in the shared worktree (two sessions running the gates together) let run B back up a file run A had # already mutated, and B's "restore" then wrote A's decoy back for good: website/en/apps.html was left without its # Radicale logo, one step from a push. An exclusive lock makes a second run WAIT for the first. import fcntl # The lock lives in THIS worktree's git dir: in a linked worktree (`git worktree add`) `.git` is a FILE, and the old # `ROOT/.git/decoy-suite.lock` crashed with NotADirectoryError (2026-10-08). Each worktree mutates its own files, so a # per-worktree lock is the right scope. try: _GITDIR = subprocess.run(["git", "rev-parse", "--absolute-git-dir"], cwd=ROOT, capture_output=True, text=True, check=True).stdout.strip() except (OSError, subprocess.CalledProcessError): _GITDIR = os.path.join(ROOT, ".git") _LOCK = open(os.path.join(_GITDIR, "decoy-suite.lock"), "w") fcntl.flock(_LOCK, fcntl.LOCK_EX) # ── WHAT THIS FILE COVERS ──────────────────────────────────────────────────────────────────────── # Read by scripts/decoy_coverage_gate.py, which AST-parses this literal rather than grepping for # gate names — a substring search for coverage would be the very shape this sweep exists to find. # A gate named here MUST have a decoy below that has been seen to fail. COVERS = { "hub-confirm": "a native confirm() in templates/partials/ (scope was os.listdir)", "manifest-bearer": "a bearer literal in manifests/overlays/ (scope was os.listdir)", "observations": "R-419: prose SAYING it carries no marker, plus both genuine markers", "reuse-refs": "a cited .go path, and (R-422) a cited .md path, that does not exist", "golden-currency": "R-410: an empty directory with a perfect name, checked by what it COUNTED", "closed-register": ("a verdict cell reading open, a row with no state cell at all, and (RULE 3, " "2026-10-03) a FINISHED row left in OPEN-ITEMS.md — with R-87's shape, a READY " "row whose prose says 'closed' about another row, asserted to PASS"), "register-shape": ("R-627: a row whose state cell was EATEN (no trailing pipe), a DUPLICATED " "id, and a blank line splitting the table — each the exact shape the " "2026-09-21 append produced; plus the genuine article, which must pass; and " "(2026-10-03) an old-shape row under the new header, a near-miss category, an " "old rank tag as Sev, an undefined state word, and a pipe outside backticks"), "decoy-coverage": "a gate registered in a runner with no decoy and no exemption (its red-proof)", "poster-facts": ("a throwaway repo where the FACTS file is committed after the poster: the gate must SAY SO; the reverse order must stay quiet; and both must exit 0, because this gate's whole contract is that it never fails a push -- the first red-proof caught it exiting 1 on a cp1250 console over one non-ASCII character in its own warning"), "iso-bootstrap": ("R-502: SIX decoys + the genuine article in scripts/test_iso_bootstrap_gate.py, run from here, docker-free (a " "fake docker on a one-directory PATH): a BLIND harness that passes a bootstrap whose " "pairing banner never paints (the R-496 shape), a failing harness, the pass line with no " "checks behind it, and no docker / no image / a docker error, each 'not checked' (exit 2); " "the gate also plants that broken banner itself in the real container on every full run"), "instructions": "R-426: a version literal in a CLAUDE.md's effective text; the same in an HTML comment must pass", "wire-contract": ("R-555: an emitted tag whose name the receiver carries ONLY in a // and a /* */ comment " "must convict (it passed for months as `language` did); the genuine article — the same " "name in a struct tag beside a `//` inside a string literal — must pass; and R-315: the " "agent RENAMING its mirror's `superseded_at` tag (the old name still occurs as a local-API " "map key) must convict on the field-by-field mirror check; and R-444: the agent renaming " "`guest_disk_trim.guests.ok` to `succeeded` (a name the hub carries elsewhere) or DROPPING `last_ok_at` (a field the " "hub reads and would show as never-ok for ever) must convict on the subtree mirror"), "stands": ("R-819: a stand citing a register id that exists in NEITHER register, and a stand " "marked 'walked' whose only source is a register row (a green dot from a label); " "plus the genuine article — a stand citing only a CLOSED row, which must PASS"), "site": ("R-423: a NEW page under website/ that PAGES does not list (the gate used to scan 7 of 9); and " "(2026-10-08, the English twin) an English nav drifting from en/index, a wrong hreflang, a " "switch at the wrong twin, lang=\"hu\" on an English page, an app missing from one apps page, " "ASCII-only Hungarian in English text and in a script message, a 'please', an English " "retrieval promise the Hungarian does not make, a FAQ JSON-LD drifting from the visible " "answer, text after , a Hungarian dashboard picture on an English page, and four picture-viewer faults (an opener without href, an opener to a missing file, openers with no gallery.js, the inline viewer put back), and (R-912) a catalog app's logo under another name than its slug — each must convict WITH its own failure line; and (2026-10-11, both of them reported from a real phone AFTER they went live) site.css's old mobile nav reaching the shell's sidebar from inside an @media block — the leak the STOP-1 check missed because it skipped @media entirely — and a