#!/usr/bin/env python3 """Tests for the graph generator's validation gate. `--validate` is the only thing standing between a typo in a note's frontmatter and a vault whose facets quietly stop meaning anything, so these tests pin the controlled vocabularies it enforces: the edge relationship types and the node `type` enum. Each filesystem test builds a throwaway vault, never the real one. """ from __future__ import annotations import importlib import subprocess import sys import tempfile import unittest from pathlib import Path SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" sys.path.insert(0, str(SCRIPTS)) gen = importlib.import_module("generate-graph") def _node(node_id: str, note_type: str) -> dict: return {"id": node_id, "path": f"{node_id}.md", "type": note_type} class TestNoteTypeVocabulary(unittest.TestCase): """The node `type` enum is the drift signal; it has to be enforced.""" def test_covers_every_type_the_taxonomy_documents(self) -> None: self.assertLessEqual( { "hub", "project", "area", "guideline", "policy", "feature", "roadmap", "decision", "note", "meta", "reference", }, gen.NOTE_TYPES, ) def test_accepts_a_graph_whose_nodes_are_all_in_the_vocabulary(self) -> None: graph = {"nodes": [_node("a", "note"), _node("b", "area")], "edges": []} self.assertEqual(gen.find_invalid_node_types(graph), []) def test_reports_a_node_whose_type_is_outside_the_vocabulary(self) -> None: graph = {"nodes": [_node("a", "note"), _node("b", "refrence")], "edges": []} invalid = gen.find_invalid_node_types(graph) self.assertEqual([n["id"] for n in invalid], ["b"]) class TestValidateExitCode(unittest.TestCase): """--validate must fail the run, not just print, on an unknown node type.""" def setUp(self) -> None: self._tmp = tempfile.TemporaryDirectory() self.root = Path(self._tmp.name).resolve() / "vault" (self.root / "scripts").mkdir(parents=True) (self.root / "03_resources").mkdir() (self.root / "scripts" / "generate-graph.py").write_text( (SCRIPTS / "generate-graph.py").read_text(encoding="utf-8"), encoding="utf-8", ) def tearDown(self) -> None: self._tmp.cleanup() def write_note(self, name: str, note_type: str) -> None: (self.root / "03_resources" / name).write_text( f"---\nid: {name[:-3]}\ntitle: {name}\ntype: {note_type}\n---\n\n# {name}\n", encoding="utf-8", ) def validate(self) -> subprocess.CompletedProcess: return subprocess.run( [sys.executable, "scripts/generate-graph.py", "--validate"], cwd=self.root, capture_output=True, text=True, ) def test_passes_on_a_vault_of_known_types(self) -> None: self.write_note("good.md", "decision") proc = self.validate() self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("all node types are in the controlled vocabulary", proc.stdout) def test_fails_and_names_the_offending_note(self) -> None: self.write_note("good.md", "decision") self.write_note("bad.md", "descision") proc = self.validate() self.assertEqual(proc.returncode, 1) self.assertIn("descision", proc.stdout) self.assertIn("bad", proc.stdout) if __name__ == "__main__": unittest.main()