test_gate.py
Language Python7.6 KB174 lines
"""gate.py: the walls, the self-check, change detection and the scaffolding rules,run against a copy of the invented sample vaults."""import jsonimport osimport reimport unittestfrom helpers import TempVaults, load, runWALLED_MARKERS = ("WALLED", "sk-FAKE")class GateTest(unittest.TestCase):def setUp(self):self.t = TempVaults()def tearDown(self):self.t.cleanup()def stage(self):return run(self.t.gate())def counts(self, out):return {k: int(v) for k, v in re.findall(r"(\w[\w-]*)=(\d+)", out)}# ---- the walls ---------------------------------------------------------def test_no_walled_file_reaches_staging(self):self.stage()staged = self.t.stagedfor name in staged:low = name.lower()for wall in ("_private", "efforts__finance", "efforts__journal", ".obsidian",".trash", ".git", ".basic-memory"):self.assertNotIn(wall, low, name)text = (self.t.staging_root / "staging" / name).read_text()for marker in WALLED_MARKERS:self.assertNotIn(marker, text, name)def test_only_note_suffixes_stage(self):self.stage()self.assertIn("workbench__inbox.txt", self.t.staged)self.assertFalse([n for n in self.t.staged if n.endswith(".png") or n.endswith(".json")])def test_self_check_refuses_when_a_wall_is_weakened(self):gate = self.t.gate()gate.DENY_PREFIXES = [p for p in gate.DENY_PREFIXES if p != "efforts/finance"]with self.assertRaises(SystemExit) as cm:run(gate)self.assertIn("DENYLIST FAILED", str(cm.exception))self.assertEqual(self.t.staged, []) # refused before touching stagingdef test_traversal_is_denied(self):gate = self.t.gate()for rel in ("../identity/Rowan.md", "Efforts/../../identity/Rowan.md", "/../x.md"):with self.assertRaises(gate.PathDenied):gate._resolve_safe(self.t.workbench.resolve(), rel)def test_symlink_out_of_the_vault_is_denied(self):outside = self.t.dir / "outside.md"outside.write_text("# Outside\n\nWALLED: a symlink target outside the vault.\n")os.symlink(outside, self.t.workbench / "shortcut.md")self.stage()self.assertNotIn("workbench__shortcut.md", self.t.staged)def test_extra_deny_from_env(self):env = dict(self.t.env, EXTRA_DENY="research, atlas/glaze-library/")run(load("gate.py", env))self.assertFalse([n for n in self.t.staged if "research" in n or "glaze-library" in n])# ---- staging and change detection -------------------------------------def test_flattened_names_encode_the_path(self):self.stage()self.assertIn("workbench__Efforts__garden-studio__notes__raised-beds.md", self.t.staged)self.assertIn("identity__Tess Okonjo.md", self.t.staged)def test_staging_is_wiped_every_run(self):self.stage()ghost = self.t.staging_root / "staging" / "ghost-from-yesterday.md"ghost.write_text("left behind")self.stage()self.assertFalse(ghost.exists())def test_rerun_stages_nothing(self):_, first = self.stage()_, second = self.stage()self.assertGreater(self.counts(first)["staged"], 0)self.assertEqual(self.counts(second)["staged"], 0)self.assertEqual(self.t.staged, [])def test_editing_one_note_stages_exactly_that_note(self):self.stage()note = self.t.identity / "Studio Glaze Library.md"note.write_text(note.read_text() + "- [status] 15 of about 40 glazes documented\n")_, out = self.stage()self.assertEqual(self.counts(out)["staged"], 1)self.assertEqual(self.t.staged, ["identity__Studio Glaze Library.md"])def test_deleted_note_leaves_the_manifest(self):self.stage()(self.t.workbench / "research" / "lake-history.md").unlink()self.stage()manifest = json.loads((self.t.staging_root / ".manifest.json").read_text())self.assertNotIn("workbench/research/lake-history.md", manifest)def test_manifest_hashes_post_gate_content(self):gate = self.t.gate()run(gate)manifest = json.loads((self.t.staging_root / ".manifest.json").read_text())me = (self.t.workbench / "me.md").read_text(encoding="utf-8")self.assertEqual(manifest["workbench/me.md"], gate._hash(gate._gate_content(me)))# ---- scaffolding ------------------------------------------------------def test_scaffolding_is_skipped(self):self.stage()for skipped in ("workbench__CLAUDE.md", "workbench__HEARTBEAT.md", "workbench__MEMORY.md","workbench__00-Index.md", "workbench__2026-09-20.md","workbench__kiln-repair-done.md", "workbench___templates__daily.md","workbench___scratch__idea.md","workbench__Efforts__garden-studio__02-run-logs__2026-09-14.md","workbench__Efforts__pottery-class__01-sessions__week-1.md"):self.assertNotIn(skipped, self.t.staged)def test_one_explainer_per_vault_in_priority_order(self):self.stage()s = self.t.stagedself.assertIn("workbench__README.md", s) # README beats allself.assertIn("workbench__Efforts__garden-studio__README.md", s)self.assertNotIn("workbench__Efforts__garden-studio__CLAUDE.md", s)self.assertIn("workbench__Efforts__bike-restoration__00-Index.md", s) # no READMEself.assertNotIn("workbench__Efforts__bike-restoration__CLAUDE.md", s)self.assertIn("workbench__Efforts__pottery-class__CLAUDE.md", s) # CLAUDE is all it hasself.assertIn("workbench__Atlas__glaze-library__cone-6-celadon.md", s) # no explainer: finedef test_keep_path_exception(self):self.stage()self.assertIn("workbench___meta__decisions.md", self.t.staged)self.assertIn("workbench___meta__state.md", self.t.staged)# ---- the section gate --------------------------------------------------def test_disabled_section_gate_stages_notes_whole(self):gate = self.t.gate()text = (self.t.identity / "Rowan.md").read_text()self.assertEqual(gate._gate_content(text), text)def test_empty_pattern_lists_match_nothing(self):gate = self.t.gate()self.assertIsNone(gate._SENSITIVE_RE.search("anything at all [[Rowan]]"))self.assertIsNone(gate._EXCLUDED_LINK_RE.search("[[Rowan]] [[Tess Okonjo]]"))def test_enabled_section_gate_stages_framing_only(self):gate = self.t.gate()gate._SENSITIVE_RE = re.compile(r"mentors", re.IGNORECASE)text = (self.t.identity / "Rowan.md").read_text()framed = gate._gate_content(text)self.assertIn("The owner of both vaults", framed)self.assertNotIn("## Observations", framed)self.assertNotIn("mentors", framed)def test_excluded_links_are_redacted_in_framing(self):gate = self.t.gate()gate._EXCLUDED_LINK_RE = re.compile(r"\[\[\s*(Tess Okonjo)", re.IGNORECASE)framed = gate._gate_content("# Note\n\nSee [[Tess Okonjo]] for details.\n\n## Relations\n- x -> y\n")self.assertIn("[[redacted", framed)self.assertNotIn("Tess", framed)def test_full_sample_run_counts(self):_, out = self.stage()c = self.counts(out)self.assertEqual(c["staged"], 21)self.assertEqual(c["total-staged"], 21)if __name__ == "__main__":unittest.main()