Files
knowledge-base-framework/tests/test_kb_framework.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

163 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Tests for the importable `kb_framework` package.
The package is the contract both the stdio MCP server and the hosted connector
consume: build the graph in memory from a vault's Markdown, answer the query
tools over that graph, and read framework instructions. These tests pin the
graph shape against a golden fixture produced by the file-writing generator,
and the tool payloads against goldens captured from the stdio server, so the
two front ends cannot drift apart.
"""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
FRAMEWORK = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(FRAMEWORK))
FIXTURES = FRAMEWORK / "tests" / "fixtures"
VAULT = FIXTURES / "vault"
from kb_framework import graph as kbg # noqa: E402
from kb_framework import query as kbq # noqa: E402
from kb_framework import query_tools as kbt # noqa: E402
from kb_framework import vault as kbv # noqa: E402
from kb_framework import instructions as kbi # noqa: E402
def golden_graph() -> dict:
return json.loads((FIXTURES / "graph_golden.json").read_text(encoding="utf-8"))
def golden_stdio() -> dict:
return json.loads((FIXTURES / "stdio_golden.json").read_text(encoding="utf-8"))
class TestBuildGraphInMemory(unittest.TestCase):
def test_matches_golden_without_touching_disk(self) -> None:
result = kbg.build_graph(VAULT)
self.assertEqual(result.graph["nodes"], golden_graph()["nodes"])
self.assertEqual(result.graph["edges"], golden_graph()["edges"])
self.assertEqual(result.graph["version"], 1)
self.assertTrue(result.graph["generated_at"])
self.assertFalse((VAULT / "index").exists())
def test_reports_no_problems_for_clean_fixture(self) -> None:
result = kbg.build_graph(VAULT)
self.assertEqual(result.unresolved, [])
self.assertEqual(result.wiki_uses, [])
self.assertEqual(kbg.validation_problems(result), [])
def test_validation_reports_unresolved_and_bad_types(self) -> None:
import tempfile
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "a.md").write_text(
"---\nid: a\ntype: descision\n---\n[x](missing){\"type\":\"bogus\"}\n[[legacy]]\n",
encoding="utf-8",
)
result = kbg.build_graph(root)
problems = "\n".join(kbg.validation_problems(result))
self.assertIn("missing", problems)
self.assertIn("descision", problems)
self.assertIn("legacy", problems)
self.assertFalse(result.ok)
class TestQueryFunctions(unittest.TestCase):
def setUp(self) -> None:
self.graph = golden_graph()
self.stdio = golden_stdio()
def strip(self, payload: dict) -> dict:
payload = dict(payload)
payload.pop("generated_at", None)
return payload
def test_summary_matches_stdio_golden(self) -> None:
case = self.stdio["graph_summary"]
payload = kbq.summary(self.graph, **case["args"])
self.assertEqual(self.strip(payload), case["structuredContent"])
def test_search_matches_stdio_golden(self) -> None:
case = self.stdio["graph_search_nodes"]
self.assertEqual(kbq.search_nodes(self.graph, case["args"]), case["structuredContent"])
def test_neighbors_matches_stdio_golden(self) -> None:
case = self.stdio["graph_neighbors"]
self.assertEqual(kbq.neighbors(self.graph, **case["args"]), case["structuredContent"])
def test_neighbors_unknown_id_raises(self) -> None:
with self.assertRaises(ValueError):
kbq.neighbors(self.graph, "nope")
def test_node_path_resolves_id(self) -> None:
self.assertEqual(kbq.node_path(self.graph, "area-ops"), "02_areas/ops.md")
with self.assertRaises(ValueError):
kbq.node_path(self.graph, "nope")
class TestVaultHelpers(unittest.TestCase):
def test_resolve_in_root_rejects_escape_and_absolute(self) -> None:
for bad in ("../x.md", "/etc/passwd", "", " "):
with self.subTest(bad=bad), self.assertRaises(ValueError):
kbv.resolve_in_root(VAULT, bad)
self.assertEqual(kbv.resolve_in_root(VAULT, "README.md"), VAULT / "README.md")
def test_read_note_payload_text_and_binary(self) -> None:
text = kbv.read_note_payload(VAULT, "02_areas/ops.md")
self.assertEqual(text["binary"], False)
self.assertIn("# Area Ops", text["content"])
binary = kbv.read_note_payload(VAULT, "03_resources/demo/raw/paper.pdf")
self.assertTrue(binary["binary"])
self.assertEqual(set(binary), {"path", "binary", "bytes", "sha256", "hint"})
with self.assertRaises(FileNotFoundError):
kbv.read_note_payload(VAULT, "nope.md")
class TestQueryTools(unittest.TestCase):
"""The five read-only tools, as the stdio server and the connector expose them."""
def setUp(self) -> None:
self.graph = golden_graph()
self.stdio = golden_stdio()
def test_definitions_match_stdio(self) -> None:
self.assertEqual(kbt.definitions(), self.stdio["tool_definitions"])
def test_call_returns_stdio_result_shape(self) -> None:
for key in ("graph_summary", "graph_search_nodes", "graph_neighbors", "read_note_by_id", "read_note_by_path"):
case = self.stdio[key]
with self.subTest(tool=key):
result = kbt.call(case["tool"], case["args"], graph=self.graph, root=VAULT)
sc = dict(result["structuredContent"])
sc.pop("generated_at", None)
self.assertEqual(sc, case["structuredContent"])
self.assertEqual(result["content"][0]["type"], "text")
self.assertEqual(json.loads(result["content"][0]["text"]).get("path", sc.get("path")), sc.get("path"))
def test_read_note_requires_exactly_one_selector(self) -> None:
for args in ({}, {"id": "area-ops", "path": "README.md"}):
with self.subTest(args=args), self.assertRaises(ValueError):
kbt.call("read_note", args, graph=self.graph, root=VAULT)
def test_unknown_tool_raises(self) -> None:
with self.assertRaises(ValueError):
kbt.call("write_note", {}, graph=self.graph, root=VAULT)
class TestInstructions(unittest.TestCase):
def test_reads_framework_documents(self) -> None:
payload = kbi.read_instructions()
self.assertEqual([d["path"] for d in payload["documents"]], ["AGENTS.md", "AGENT_QUERY_GUIDE.md"])
self.assertIn("AGENT_QUERY_GUIDE", payload["documents"][0]["content"])
self.assertTrue(Path(payload["framework_root"]).is_dir())
if __name__ == "__main__":
unittest.main()