mattrguerra/better-memory-pipelinemainPrivate on GitHub, published here in full

test_gate.py

Language Python7.6 KB174 lines

tests/test_gate.py

  1. """gate.py: the walls, the self-check, change detection and the scaffolding rules,
  2. run against a copy of the invented sample vaults."""
  3. import json
  4. import os
  5. import re
  6. import unittest
  7. from helpers import TempVaults, load, run
  8. WALLED_MARKERS = ("WALLED", "sk-FAKE")
  9. class GateTest(unittest.TestCase):
  10. def setUp(self):
  11. self.t = TempVaults()
  12. def tearDown(self):
  13. self.t.cleanup()
  14. def stage(self):
  15. return run(self.t.gate())
  16. def counts(self, out):
  17. return {k: int(v) for k, v in re.findall(r"(\w[\w-]*)=(\d+)", out)}
  18. # ---- the walls ---------------------------------------------------------
  19. def test_no_walled_file_reaches_staging(self):
  20. self.stage()
  21. staged = self.t.staged
  22. for name in staged:
  23. low = name.lower()
  24. for wall in ("_private", "efforts__finance", "efforts__journal", ".obsidian",
  25. ".trash", ".git", ".basic-memory"):
  26. self.assertNotIn(wall, low, name)
  27. text = (self.t.staging_root / "staging" / name).read_text()
  28. for marker in WALLED_MARKERS:
  29. self.assertNotIn(marker, text, name)
  30. def test_only_note_suffixes_stage(self):
  31. self.stage()
  32. self.assertIn("workbench__inbox.txt", self.t.staged)
  33. self.assertFalse([n for n in self.t.staged if n.endswith(".png") or n.endswith(".json")])
  34. def test_self_check_refuses_when_a_wall_is_weakened(self):
  35. gate = self.t.gate()
  36. gate.DENY_PREFIXES = [p for p in gate.DENY_PREFIXES if p != "efforts/finance"]
  37. with self.assertRaises(SystemExit) as cm:
  38. run(gate)
  39. self.assertIn("DENYLIST FAILED", str(cm.exception))
  40. self.assertEqual(self.t.staged, []) # refused before touching staging
  41. def test_traversal_is_denied(self):
  42. gate = self.t.gate()
  43. for rel in ("../identity/Rowan.md", "Efforts/../../identity/Rowan.md", "/../x.md"):
  44. with self.assertRaises(gate.PathDenied):
  45. gate._resolve_safe(self.t.workbench.resolve(), rel)
  46. def test_symlink_out_of_the_vault_is_denied(self):
  47. outside = self.t.dir / "outside.md"
  48. outside.write_text("# Outside\n\nWALLED: a symlink target outside the vault.\n")
  49. os.symlink(outside, self.t.workbench / "shortcut.md")
  50. self.stage()
  51. self.assertNotIn("workbench__shortcut.md", self.t.staged)
  52. def test_extra_deny_from_env(self):
  53. env = dict(self.t.env, EXTRA_DENY="research, atlas/glaze-library/")
  54. run(load("gate.py", env))
  55. self.assertFalse([n for n in self.t.staged if "research" in n or "glaze-library" in n])
  56. # ---- staging and change detection -------------------------------------
  57. def test_flattened_names_encode_the_path(self):
  58. self.stage()
  59. self.assertIn("workbench__Efforts__garden-studio__notes__raised-beds.md", self.t.staged)
  60. self.assertIn("identity__Tess Okonjo.md", self.t.staged)
  61. def test_staging_is_wiped_every_run(self):
  62. self.stage()
  63. ghost = self.t.staging_root / "staging" / "ghost-from-yesterday.md"
  64. ghost.write_text("left behind")
  65. self.stage()
  66. self.assertFalse(ghost.exists())
  67. def test_rerun_stages_nothing(self):
  68. _, first = self.stage()
  69. _, second = self.stage()
  70. self.assertGreater(self.counts(first)["staged"], 0)
  71. self.assertEqual(self.counts(second)["staged"], 0)
  72. self.assertEqual(self.t.staged, [])
  73. def test_editing_one_note_stages_exactly_that_note(self):
  74. self.stage()
  75. note = self.t.identity / "Studio Glaze Library.md"
  76. note.write_text(note.read_text() + "- [status] 15 of about 40 glazes documented\n")
  77. _, out = self.stage()
  78. self.assertEqual(self.counts(out)["staged"], 1)
  79. self.assertEqual(self.t.staged, ["identity__Studio Glaze Library.md"])
  80. def test_deleted_note_leaves_the_manifest(self):
  81. self.stage()
  82. (self.t.workbench / "research" / "lake-history.md").unlink()
  83. self.stage()
  84. manifest = json.loads((self.t.staging_root / ".manifest.json").read_text())
  85. self.assertNotIn("workbench/research/lake-history.md", manifest)
  86. def test_manifest_hashes_post_gate_content(self):
  87. gate = self.t.gate()
  88. run(gate)
  89. manifest = json.loads((self.t.staging_root / ".manifest.json").read_text())
  90. me = (self.t.workbench / "me.md").read_text(encoding="utf-8")
  91. self.assertEqual(manifest["workbench/me.md"], gate._hash(gate._gate_content(me)))
  92. # ---- scaffolding ------------------------------------------------------
  93. def test_scaffolding_is_skipped(self):
  94. self.stage()
  95. for skipped in ("workbench__CLAUDE.md", "workbench__HEARTBEAT.md", "workbench__MEMORY.md",
  96. "workbench__00-Index.md", "workbench__2026-09-20.md",
  97. "workbench__kiln-repair-done.md", "workbench___templates__daily.md",
  98. "workbench___scratch__idea.md",
  99. "workbench__Efforts__garden-studio__02-run-logs__2026-09-14.md",
  100. "workbench__Efforts__pottery-class__01-sessions__week-1.md"):
  101. self.assertNotIn(skipped, self.t.staged)
  102. def test_one_explainer_per_vault_in_priority_order(self):
  103. self.stage()
  104. s = self.t.staged
  105. self.assertIn("workbench__README.md", s) # README beats all
  106. self.assertIn("workbench__Efforts__garden-studio__README.md", s)
  107. self.assertNotIn("workbench__Efforts__garden-studio__CLAUDE.md", s)
  108. self.assertIn("workbench__Efforts__bike-restoration__00-Index.md", s) # no README
  109. self.assertNotIn("workbench__Efforts__bike-restoration__CLAUDE.md", s)
  110. self.assertIn("workbench__Efforts__pottery-class__CLAUDE.md", s) # CLAUDE is all it has
  111. self.assertIn("workbench__Atlas__glaze-library__cone-6-celadon.md", s) # no explainer: fine
  112. def test_keep_path_exception(self):
  113. self.stage()
  114. self.assertIn("workbench___meta__decisions.md", self.t.staged)
  115. self.assertIn("workbench___meta__state.md", self.t.staged)
  116. # ---- the section gate --------------------------------------------------
  117. def test_disabled_section_gate_stages_notes_whole(self):
  118. gate = self.t.gate()
  119. text = (self.t.identity / "Rowan.md").read_text()
  120. self.assertEqual(gate._gate_content(text), text)
  121. def test_empty_pattern_lists_match_nothing(self):
  122. gate = self.t.gate()
  123. self.assertIsNone(gate._SENSITIVE_RE.search("anything at all [[Rowan]]"))
  124. self.assertIsNone(gate._EXCLUDED_LINK_RE.search("[[Rowan]] [[Tess Okonjo]]"))
  125. def test_enabled_section_gate_stages_framing_only(self):
  126. gate = self.t.gate()
  127. gate._SENSITIVE_RE = re.compile(r"mentors", re.IGNORECASE)
  128. text = (self.t.identity / "Rowan.md").read_text()
  129. framed = gate._gate_content(text)
  130. self.assertIn("The owner of both vaults", framed)
  131. self.assertNotIn("## Observations", framed)
  132. self.assertNotIn("mentors", framed)
  133. def test_excluded_links_are_redacted_in_framing(self):
  134. gate = self.t.gate()
  135. gate._EXCLUDED_LINK_RE = re.compile(r"\[\[\s*(Tess Okonjo)", re.IGNORECASE)
  136. framed = gate._gate_content("# Note\n\nSee [[Tess Okonjo]] for details.\n\n## Relations\n- x -> y\n")
  137. self.assertIn("[[redacted", framed)
  138. self.assertNotIn("Tess", framed)
  139. def test_full_sample_run_counts(self):
  140. _, out = self.stage()
  141. c = self.counts(out)
  142. self.assertEqual(c["staged"], 21)
  143. self.assertEqual(c["total-staged"], 21)
  144. if __name__ == "__main__":
  145. unittest.main()