Files
knowledge-base-framework/tests/test_generate_graph.py
TotenKnopfandClaude Fable 5.1 96780317a7 refactor: extract kb_framework package, build graph in memory
Scripts become thin CLIs over kb_framework (graph, query, query_tools,
vault, instructions) so the hosted read-only connector shares one
implementation with the stdio server. The MCP server builds the graph
from Markdown on demand and invalidates it on writes; refresh_graph
rebuilds in memory and reports validation problems. Pre-commit hook
validates only; index/graph.json is no longer read, written or staged
(generate-graph.py --write remains for operators). Golden fixtures pin
graph and tool payloads to the previous output. Agent and skill docs
updated to match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 20:40:13 +02:00

102 lines
3.4 KiB
Python

#!/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 / "03_resources").mkdir(parents=True)
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, str(SCRIPTS / "generate-graph.py"), "--vault-root", str(self.root), "--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()