Files
knowledge-base-framework/tests/test_in_memory_graph.py
T
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

125 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Tests for the graph-in-memory contract of the stdio server and the hook.
`index/graph.json` is no longer a committed artifact: the MCP server builds
the graph lazily from Markdown, invalidates it on writes, and `refresh_graph`
rebuilds (and optionally validates) in memory. The pre-commit hook only
validates. These tests pin that nothing in the default flow writes the file.
"""
from __future__ import annotations
import importlib
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
FRAMEWORK = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(FRAMEWORK / "scripts"))
mcp = importlib.import_module("mcp-server")
def write_note(root: Path, rel: str, note_id: str, body: str = "") -> None:
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f"---\nid: {note_id}\ntitle: {note_id}\ntype: note\n---\n# {note_id}\n{body}\n", encoding="utf-8")
class InMemoryVault(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name).resolve() / "vault"
self.root.mkdir()
write_note(self.root, "03_resources/a.md", "a", "[b](b)")
write_note(self.root, "03_resources/b.md", "b")
self.state = mcp.AppState(root=self.root)
def tearDown(self) -> None:
self._tmp.cleanup()
def call(self, name: str, **args) -> dict:
return mcp._call_tool(self.state, name, args)
class TestStdioServerBuildsGraphLazily(InMemoryVault):
def test_summary_without_graph_file(self) -> None:
payload = self.call("graph_summary")["structuredContent"]
self.assertEqual(payload["node_count"], 2)
self.assertEqual(payload["edge_count"], 1)
self.assertFalse((self.root / "index").exists())
def test_write_note_invalidates_cache(self) -> None:
self.call("graph_summary")
self.call("write_note", path="03_resources/c.md", content="---\nid: c\n---\n# c\n")
self.assertEqual(self.call("graph_summary")["structuredContent"]["node_count"], 3)
def test_edit_note_invalidates_cache(self) -> None:
self.call("graph_summary")
self.call("edit_note", path="03_resources/b.md", old_string="# b", new_string="# b\n[a](a)")
self.assertEqual(self.call("graph_summary")["structuredContent"]["edge_count"], 2)
def test_refresh_rebuilds_and_validates_in_memory(self) -> None:
self.call("graph_summary")
write_note(self.root, "03_resources/d.md", "d", "[gone](nowhere)")
ok = self.call("refresh_graph")
self.assertFalse(ok.get("isError"), ok)
self.assertEqual(ok["structuredContent"]["node_count"], 3)
bad = self.call("refresh_graph", validate=True)
self.assertTrue(bad.get("isError"), bad)
self.assertIn("nowhere", json.dumps(bad["structuredContent"]))
self.assertFalse((self.root / "index").exists())
def test_graph_resource_is_served_from_memory(self) -> None:
result = mcp._read_resource(self.state, "repo://graph")
graph = json.loads(result["contents"][0]["text"])
self.assertEqual(len(graph["nodes"]), 2)
self.assertFalse((self.root / "index").exists())
def test_read_note_by_id_uses_in_memory_graph(self) -> None:
payload = self.call("read_note", id="b")["structuredContent"]
self.assertEqual(payload["path"], "03_resources/b.md")
class TestGeneratorAndHookValidateOnly(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name).resolve() / "vault"
self.root.mkdir()
write_note(self.root, "03_resources/a.md", "a")
self.env = {k: v for k, v in os.environ.items() if k != "KNOWLEDGE_BASE_ROOT"}
def tearDown(self) -> None:
self._tmp.cleanup()
def run_cmd(self, *cmd: str) -> subprocess.CompletedProcess:
return subprocess.run(list(cmd), cwd=self.root, env=self.env, capture_output=True, text=True)
def test_validate_alone_writes_nothing(self) -> None:
proc = self.run_cmd(sys.executable, str(FRAMEWORK / "scripts/generate-graph.py"), "--vault-root", str(self.root), "--validate")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertFalse((self.root / "index").exists())
def test_write_still_produces_a_file_for_operators(self) -> None:
proc = self.run_cmd(sys.executable, str(FRAMEWORK / "scripts/generate-graph.py"), "--vault-root", str(self.root), "--write")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertTrue((self.root / "index/graph.json").exists())
def test_hook_validates_without_requiring_a_staged_graph(self) -> None:
hook = FRAMEWORK / "scripts/pre-commit"
proc = self.run_cmd("bash", str(hook), str(self.root))
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertFalse((self.root / "index").exists())
write_note(self.root, "03_resources/b.md", "b", "[x](missing)")
proc = self.run_cmd("bash", str(hook), str(self.root))
self.assertNotEqual(proc.returncode, 0)
self.assertIn("missing", proc.stdout + proc.stderr)
self.assertFalse((self.root / "index").exists())
if __name__ == "__main__":
unittest.main()