#!/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()