gate: instructions_gate check 6 — the auto-memory index (R-229)
gates / gates (push) Successful in 15s

MEMORY.md is the larger half of what loads before a word is typed (8.4k tokens vs the root
CLAUDE.md's 6.6k) and is the one instruction file nobody hand-edits, so nothing was watching it.

Three deliberately different outcomes, each pinned by a test: over-ceiling FAILS (auto-memory
drops content past the limit with no error), an orphan WARNS (the store is outside git), and an
absent store PASSES while PRINTING its reason -- asserted on the reason text, because a pass with
no reason is indistinguishable from a gate that stopped running.

39 assertions (was 20). Red-proof run against the real store, not a fixture.
This commit is contained in:
2026-08-06 10:48:59 +02:00
parent 3a9dd81e18
commit f27aed87cd
3 changed files with 265 additions and 1 deletions
+111
View File
@@ -184,6 +184,117 @@ def test_diverged_workspace_copy_fails():
check("message names both files", "workspace-CLAUDE.md" in out)
# --- memory-index checks (gate check 6) ---------------------------------------------------------
#
# The three outcomes are deliberately different and each has a test proving it is the one intended:
# over-limit FAILS (silent truncation), an orphan WARNS (the store is outside git), and an absent
# store PASSES *while printing its reason* (machine-local by design). The last one is the dangerous
# grant — an exception to "a missing input is a FAILURE" — so it is asserted on the reason text, not
# just on rc == 0. A pass with no reason is indistinguishable from a gate that stopped running.
def make_memory(tmp, index, topics=None, archived=None):
"""Create <tmp>/.claude-memory. The gate resolves the store from the WORKSPACE root, which for
make_repo(tmp, ...) is tmp itself."""
store = os.path.join(tmp, ".claude-memory")
os.makedirs(store, exist_ok=True)
if index is not None:
with open(os.path.join(store, "MEMORY.md"), "w", encoding="utf-8") as fh:
fh.write(index)
for name, body in (topics or {}).items():
with open(os.path.join(store, name), "w", encoding="utf-8") as fh:
fh.write(body)
if archived:
arc = os.path.join(store, "archive")
os.makedirs(arc, exist_ok=True)
for name, body in archived.items():
with open(os.path.join(arc, name), "w", encoding="utf-8") as fh:
fh.write(body)
return store
def test_memory_absent_passes_and_prints_the_reason():
with tempfile.TemporaryDirectory() as tmp:
root = make_repo(tmp, "# Repo\n") # no .claude-memory at all
rc, out = run_gate(root)
check("absent memory store exits 0", rc == 0, "rc=%d" % rc)
check("absent memory store PRINTS its reason", "machine-local by design" in out)
check("absent memory store is not reported as a skip", "PASS with reason" in out)
def test_memory_within_limits_passes():
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [a](a.md)\n", topics={"a.md": "# A\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("memory index within limits exits 0", rc == 0, "rc=%d" % rc)
check("tally reports indexed/orphaned/archived", "1 indexed, 0 orphaned" in out)
def test_memory_over_line_limit_fails_and_names_file_and_count():
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n" + "- entry\n" * 250)
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("over-length memory index exits non-zero", rc != 0, "rc=%d" % rc)
check("message names MEMORY.md", "MEMORY.md" in out)
check("message names the line count", "251 lines" in out)
check("message says silent truncation, not space", "DROPPED WITH NO" in out)
def test_memory_over_byte_limit_fails():
"""Few lines, many bytes — the byte ceiling must bite independently of the line ceiling."""
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n" + ("x" * 30000) + "\n")
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("over-byte memory index exits non-zero", rc != 0, "rc=%d" % rc)
check("byte failure names the byte ceiling", "25600" in out)
def test_memory_orphan_warns_but_does_not_fail():
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [a](a.md)\n",
topics={"a.md": "# A\n", "lonely.md": "# Lonely\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("an orphan does NOT fail the gate", rc == 0, "rc=%d" % rc)
check("an orphan produces a WARNING", "WARNING" in out)
check("the warning names the orphan", "lonely.md" in out)
def test_memory_wikilink_counts_as_a_reference():
"""Load-bearing negative: a file reachable only via [[wikilink]] is indexed, not orphaned.
Calling it an orphan would push someone to add a duplicate row for it."""
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [a](a.md) — see [[deep]]\n",
topics={"a.md": "# A\n", "deep.md": "# Deep\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("wikilink-only file is not an orphan", rc == 0 and "WARNING" not in out, "rc=%d" % rc)
def test_memory_archived_file_is_not_an_orphan():
"""archive/ is where deliberately set-aside records live; they are not top-level and must not
be counted as unreferenced."""
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [a](a.md)\n", topics={"a.md": "# A\n"},
archived={"old-campaign.md": "# Old\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("archived file is not an orphan", rc == 0 and "WARNING" not in out, "rc=%d" % rc)
check("tally counts the archived file", "1 archived" in out)
def test_memory_store_without_index_fails():
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, None, topics={"a.md": "# A\n"}) # store exists, no MEMORY.md
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("store with no index exits non-zero", rc != 0, "rc=%d" % rc)
check("message says unreachable knowledge", "unreachable knowledge" in out)
def main():
print("test_instructions_gate")
for fn in sorted(