c21bcf84f7
gates / gates (push) Successful in 7s
New shared scripts/instructions_gate.py, registered in controller_gates.py and agent_gates.py, never copied into a sibling repo (the reuse_refs_check.py precedent). 20 fixture tests, all asserting the effect: exit code AND that the message names the file and the reason. It is a consistency gate, not a budget gate, and the failure message says so. A /context reading measured the instruction files at 15k tokens against 869k free in a 1M window -- space is not the constraint, and a future reader must not re-derive the wrong reason. The 200-line ceiling is adherence guidance; a file nobody can hold in their head is where contradictions hide, and five were found here. Checks run against effective text (HTML comments stripped, because they are stripped before injection): the line ceiling; every .claude/rules/*.md declares paths: or an explicit unconditional: true; no component version literal; no TEMPORARY block carrying a past date; and the workspace-root CLAUDE.md is byte-identical to its versioned copy -- the live file sits outside any git repo, so that copy is its only version-controlled record. Two traps recorded so they are not reintroduced: a bare \d+\.\d+\.\d+ matches the first three octets of every IPv4 (the gate excludes dotted quads, or it fails on 192.168.0.180 in the agent's own file); and unconditional: true is NOT a Claude Code feature but this project's own marker. Workspace-root CLAUDE.md 208 -> 182 lines (142 effective), copy kept identical. The nine-instance invariant table moved into the felhom-testing skill, which triggers when writing or reviewing a test; all three directive bullets stayed in the core. felhom.eu/CLAUDE.md got surgical corrections only and is knowingly still over the ceiling at 227 effective lines -- closing it needs the restructure R-229 defers, said plainly rather than quietly absorbed. CONTEXT.md gains standing ruling S-35. OPEN-ITEMS.md gains R-229. Docs only -- no Go, no version bump, nothing built or deployed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJc8sAGRWmavP3rMtdpkr2
201 lines
7.2 KiB
Python
201 lines
7.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Fixture tests for instructions_gate.py.
|
|
|
|
Run: python3 scripts/test_instructions_gate.py
|
|
|
|
Every test asserts the EFFECT — the gate's exit code AND that its message names the file and the
|
|
reason — not merely that "it ran". A gate that exits non-zero for the wrong reason is not a gate.
|
|
|
|
The negatives matter as much as the positives here: the whole point of the HTML-comment convention
|
|
is that commented content costs nothing, so `test_comments_do_not_count_toward_ceiling` and
|
|
`test_version_literal_in_comment_is_allowed` are what stop the gate from punishing the very move it
|
|
is meant to encourage.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
GATE = os.path.join(HERE, "instructions_gate.py")
|
|
|
|
PASSED = []
|
|
FAILED = []
|
|
|
|
|
|
def run_gate(root, today="2026-08-06"):
|
|
env = dict(os.environ, FELHOM_GATE_TODAY=today)
|
|
p = subprocess.run(
|
|
[sys.executable, GATE, root],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
return p.returncode, p.stdout + p.stderr
|
|
|
|
|
|
def make_repo(tmp, claude_md, rules=None):
|
|
root = os.path.join(tmp, "repo")
|
|
os.makedirs(root, exist_ok=True)
|
|
with open(os.path.join(root, "CLAUDE.md"), "w", encoding="utf-8") as fh:
|
|
fh.write(claude_md)
|
|
if rules:
|
|
rd = os.path.join(root, ".claude", "rules")
|
|
os.makedirs(rd, exist_ok=True)
|
|
for name, body in rules.items():
|
|
with open(os.path.join(rd, name), "w", encoding="utf-8") as fh:
|
|
fh.write(body)
|
|
return root
|
|
|
|
|
|
def check(name, cond, detail=""):
|
|
(PASSED if cond else FAILED).append(name + ((" — " + detail) if detail else ""))
|
|
print((" PASS " if cond else " FAIL ") + name + ((" " + detail) if detail else ""))
|
|
|
|
|
|
def test_short_file_passes():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(tmp, "# Repo\n" + "a line\n" * 150)
|
|
rc, out = run_gate(root)
|
|
check("150-line CLAUDE.md exits 0", rc == 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_long_file_fails_and_names_file_and_count():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(tmp, "# Repo\n" + "a line\n" * 204)
|
|
rc, out = run_gate(root)
|
|
check("205-line CLAUDE.md exits non-zero", rc != 0, "rc=%d" % rc)
|
|
check("message names the file", "CLAUDE.md" in out)
|
|
check("message names the effective line count", "205 effective lines" in out)
|
|
check(
|
|
"message says ADHERENCE, not space",
|
|
"ADHERENCE limit, not a space limit" in out,
|
|
)
|
|
|
|
|
|
def test_comments_do_not_count_toward_ceiling():
|
|
"""The convention's load-bearing negative: 400 commented lines must not trip the ceiling."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
body = "# Repo\n" + "a line\n" * 100 + "<!--\n" + "history\n" * 400 + "-->\n"
|
|
root = make_repo(tmp, body)
|
|
rc, out = run_gate(root)
|
|
check("400 commented lines do not trip the ceiling", rc == 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_rule_with_paths_passes():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(
|
|
tmp,
|
|
"# Repo\n",
|
|
rules={"scoped.md": '---\npaths: ["**/*.go"]\n---\n\n# Scoped\n'},
|
|
)
|
|
rc, out = run_gate(root)
|
|
check("rule with paths: exits 0", rc == 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_rule_without_paths_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(
|
|
tmp, "# Repo\n", rules={"bare.md": "---\nname: bare\n---\n\n# Bare\n"}
|
|
)
|
|
rc, out = run_gate(root)
|
|
check("rule with neither marker exits non-zero", rc != 0, "rc=%d" % rc)
|
|
check("message names the rule file", "bare.md" in out)
|
|
|
|
|
|
def test_rule_with_unconditional_marker_passes():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(
|
|
tmp,
|
|
"# Repo\n",
|
|
rules={"always.md": "---\nunconditional: true\n---\n\n# Always\n"},
|
|
)
|
|
rc, out = run_gate(root)
|
|
check("explicit unconditional: true exits 0", rc == 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_version_literal_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(tmp, "# Repo\n\nThe box runs agent 0.93.0 today.\n")
|
|
rc, out = run_gate(root)
|
|
check("version literal exits non-zero", rc != 0, "rc=%d" % rc)
|
|
check("message quotes the version", "0.93.0" in out)
|
|
|
|
|
|
def test_ipv4_is_not_a_version():
|
|
"""A bare \\d+\\.\\d+\\.\\d+ matches the first three octets of every IPv4."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(tmp, "# Repo\n\nDooPlex is 192.168.0.180 and the demo box is 10.0.0.1.\n")
|
|
rc, out = run_gate(root)
|
|
check("IPv4 addresses are not flagged as versions", rc == 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_version_literal_in_comment_is_allowed():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(tmp, "# Repo\n\n<!-- fixed in hub v0.97.0 -->\n")
|
|
rc, out = run_gate(root)
|
|
check("version literal inside a comment is allowed", rc == 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_expired_temporary_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(
|
|
tmp,
|
|
"# Repo\n\n> **TEMPORARY — host is away (until ~2026-08-02).**\n> Delete on return.\n",
|
|
)
|
|
rc, out = run_gate(root)
|
|
check("expired TEMPORARY block exits non-zero", rc != 0, "rc=%d" % rc)
|
|
check("message quotes the past date", "2026-08-02" in out)
|
|
|
|
|
|
def test_future_temporary_passes():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = make_repo(
|
|
tmp, "# Repo\n\n> **TEMPORARY — host is away (until ~2026-12-31).**\n"
|
|
)
|
|
rc, out = run_gate(root)
|
|
check("TEMPORARY with a future date exits 0", rc == 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_missing_claude_md_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = os.path.join(tmp, "repo")
|
|
os.makedirs(root)
|
|
rc, out = run_gate(root)
|
|
check("absent CLAUDE.md is a FAILURE, not a skip", rc != 0, "rc=%d" % rc)
|
|
|
|
|
|
def test_diverged_workspace_copy_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
ws = os.path.join(tmp, "ws")
|
|
root = os.path.join(ws, "repo")
|
|
os.makedirs(root)
|
|
with open(os.path.join(root, "CLAUDE.md"), "w", encoding="utf-8") as fh:
|
|
fh.write("# Repo\n")
|
|
with open(os.path.join(ws, "CLAUDE.md"), "w", encoding="utf-8") as fh:
|
|
fh.write("# Workspace live\n")
|
|
cp = os.path.join(ws, "felhom.eu", "documentation", "runbooks")
|
|
os.makedirs(cp)
|
|
with open(os.path.join(cp, "workspace-CLAUDE.md"), "w", encoding="utf-8") as fh:
|
|
fh.write("# Workspace copy — DIVERGED\n")
|
|
rc, out = run_gate(root)
|
|
check("diverged workspace copy exits non-zero", rc != 0, "rc=%d" % rc)
|
|
check("message names both files", "workspace-CLAUDE.md" in out)
|
|
|
|
|
|
def main():
|
|
print("test_instructions_gate")
|
|
for fn in sorted(
|
|
(v for k, v in globals().items() if k.startswith("test_")),
|
|
key=lambda f: f.__name__,
|
|
):
|
|
fn()
|
|
print("")
|
|
print("passed: %d failed: %d" % (len(PASSED), len(FAILED)))
|
|
return 1 if FAILED else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|