199 lines
7.8 KiB
Python
199 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
test_parse_knowledge_base.py — Tests for title-case infra file detection and
|
|
article-root-prefixed wiki links in the Karpathy wiki parser (issue #342).
|
|
|
|
Run from the repo root:
|
|
python -m unittest tests.skill.knowledge.test_parse_knowledge_base -v
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
# ── Module loaders ────────────────────────────────────────────────────────
|
|
# The scripts have hyphens in their names, so we cannot `import` them
|
|
# directly. Load them via importlib so we can call module-level helpers.
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
_REPO_ROOT = _HERE.parent.parent.parent
|
|
_SKILL_DIR = _REPO_ROOT / "understand-anything-plugin" / "skills" / "understand-knowledge"
|
|
|
|
|
|
def _load(script: str, alias: str) -> Any:
|
|
spec = importlib.util.spec_from_file_location(alias, _SKILL_DIR / script)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError(f"Could not load {script}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[alias] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
pkb = _load("parse-knowledge-base.py", "parse_knowledge_base")
|
|
mkg = _load("merge-knowledge-graph.py", "merge_knowledge_graph")
|
|
|
|
|
|
def _fs_is_case_sensitive(tmp: Path) -> bool:
|
|
probe = tmp / "CaseProbe.md"
|
|
probe.write_text("x", encoding="utf-8")
|
|
try:
|
|
return not (tmp / "caseprobe.md").is_file()
|
|
finally:
|
|
probe.unlink()
|
|
|
|
|
|
class TestFindMarkdownCaseInsensitive(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.tmp = Path(tempfile.mkdtemp(prefix="ua-pkb-"))
|
|
|
|
def tearDown(self) -> None:
|
|
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
|
|
def test_title_case_file_is_found(self) -> None:
|
|
(self.tmp / "Index.md").write_text("# Wiki", encoding="utf-8")
|
|
found = pkb.find_markdown_case_insensitive(self.tmp, "index.md")
|
|
self.assertTrue(found.is_file())
|
|
self.assertEqual(found.name.lower(), "index.md")
|
|
|
|
def test_exact_lowercase_wins_when_both_exist(self) -> None:
|
|
if not _fs_is_case_sensitive(self.tmp):
|
|
self.skipTest("filesystem is case-insensitive; both casings collide")
|
|
(self.tmp / "Index.md").write_text("# Title", encoding="utf-8")
|
|
(self.tmp / "index.md").write_text("# lower", encoding="utf-8")
|
|
found = pkb.find_markdown_case_insensitive(self.tmp, "index.md")
|
|
self.assertEqual(found.name, "index.md")
|
|
|
|
def test_missing_parent_returns_candidate(self) -> None:
|
|
found = pkb.find_markdown_case_insensitive(self.tmp / "nope", "index.md")
|
|
self.assertFalse(found.is_file())
|
|
|
|
def test_merge_script_helper_matches(self) -> None:
|
|
(self.tmp / "Index.md").write_text("# Wiki", encoding="utf-8")
|
|
found = mkg._find_markdown_case_insensitive(self.tmp, "index.md")
|
|
self.assertTrue(found.is_file())
|
|
|
|
|
|
class TestTitleCaseWiki(unittest.TestCase):
|
|
"""A wiki using Index.md / Log.md must parse the same as index.md / log.md."""
|
|
|
|
def setUp(self) -> None:
|
|
self.tmp = Path(tempfile.mkdtemp(prefix="ua-pkb-wiki-"))
|
|
wiki = self.tmp / "wiki"
|
|
(wiki / "concepts").mkdir(parents=True)
|
|
(wiki / "projects").mkdir(parents=True)
|
|
(wiki / "Index.md").write_text(
|
|
"# Hermes\n\n## Concepts\n\n- [[wiki/concepts/Index]]\n\n"
|
|
"## Projects\n\n- [[wiki/projects/Personal Wiki Second Brain]]\n",
|
|
encoding="utf-8",
|
|
)
|
|
(wiki / "Log.md").write_text(
|
|
"# Log\n\n## [2026-05-01] CREATE | Seeded wiki\n", encoding="utf-8"
|
|
)
|
|
(wiki / "concepts" / "Index.md").write_text(
|
|
"# Concepts Index\n\nOverview of concepts.\n", encoding="utf-8"
|
|
)
|
|
(wiki / "projects" / "Personal Wiki Second Brain.md").write_text(
|
|
"# Personal Wiki Second Brain\n\nPilot project.\n", encoding="utf-8"
|
|
)
|
|
|
|
def tearDown(self) -> None:
|
|
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
|
|
def test_detect_format_sees_title_case_infra(self) -> None:
|
|
signals = pkb.detect_format(self.tmp)
|
|
self.assertTrue(signals["has_index"])
|
|
self.assertTrue(signals["has_log"])
|
|
self.assertTrue(signals["detected"])
|
|
|
|
def test_title_case_root_infra_is_not_an_article(self) -> None:
|
|
manifest = pkb.parse_wiki(self.tmp)
|
|
article_ids = {n["id"] for n in manifest["nodes"] if n["type"] == "article"}
|
|
self.assertNotIn("article:Index", article_ids)
|
|
self.assertNotIn("article:Log", article_ids)
|
|
# Nested Index.md IS content
|
|
self.assertIn("article:concepts/Index", article_ids)
|
|
|
|
def test_root_prefixed_category_links_resolve(self) -> None:
|
|
"""[[wiki/concepts/Index]] must map to article:concepts/Index when
|
|
wiki/ is the detected article root (the #342 pilot saw every node
|
|
land in "Other" because this lookup missed)."""
|
|
manifest = pkb.parse_wiki(self.tmp)
|
|
cat_edges = {
|
|
(e["source"], e["target"])
|
|
for e in manifest["edges"]
|
|
if e["type"] == "categorized_under"
|
|
}
|
|
self.assertIn(("article:concepts/Index", "topic:concepts"), cat_edges)
|
|
self.assertIn(
|
|
("article:projects/Personal Wiki Second Brain", "topic:projects"),
|
|
cat_edges,
|
|
)
|
|
by_id = {n["id"]: n for n in manifest["nodes"]}
|
|
self.assertEqual(
|
|
by_id["article:concepts/Index"]["knowledgeMeta"].get("category"),
|
|
"Concepts",
|
|
)
|
|
|
|
def test_links_outside_article_root_stay_unresolved(self) -> None:
|
|
"""maps/*, SCHEMA etc. must not be forced into article layers."""
|
|
wiki = self.tmp / "wiki"
|
|
(wiki / "concepts" / "Index.md").write_text(
|
|
"# Concepts Index\n\nSee [[maps/overview]] and [[SCHEMA]].\n",
|
|
encoding="utf-8",
|
|
)
|
|
manifest = pkb.parse_wiki(self.tmp)
|
|
related = {
|
|
e["target"] for e in manifest["edges"] if e["source"] == "article:concepts/Index"
|
|
}
|
|
self.assertNotIn("article:maps/overview", related)
|
|
self.assertTrue(any("maps/overview" in w for w in manifest["warnings"]))
|
|
|
|
|
|
class TestLowercaseWikiStillWorks(unittest.TestCase):
|
|
"""Regression guard: the original all-lowercase layout keeps parsing."""
|
|
|
|
def setUp(self) -> None:
|
|
self.tmp = Path(tempfile.mkdtemp(prefix="ua-pkb-lower-"))
|
|
(self.tmp / "concepts").mkdir(parents=True)
|
|
(self.tmp / "index.md").write_text(
|
|
"# Wiki\n\n## Concepts\n\n- [[concepts/attention]]\n", encoding="utf-8"
|
|
)
|
|
(self.tmp / "log.md").write_text("# Log\n", encoding="utf-8")
|
|
(self.tmp / "concepts" / "attention.md").write_text(
|
|
"# Attention\n\nAll you need.\n", encoding="utf-8"
|
|
)
|
|
(self.tmp / "concepts" / "transformer.md").write_text(
|
|
"# Transformer\n\nSee [[attention]].\n", encoding="utf-8"
|
|
)
|
|
|
|
def tearDown(self) -> None:
|
|
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
|
|
def test_lowercase_layout_parses_with_categories_and_edges(self) -> None:
|
|
signals = pkb.detect_format(self.tmp)
|
|
self.assertTrue(signals["detected"])
|
|
manifest = pkb.parse_wiki(self.tmp)
|
|
by_id = {n["id"]: n for n in manifest["nodes"]}
|
|
self.assertIn("article:concepts/attention", by_id)
|
|
self.assertEqual(
|
|
by_id["article:concepts/attention"]["knowledgeMeta"].get("category"),
|
|
"Concepts",
|
|
)
|
|
edge_keys = {(e["source"], e["target"], e["type"]) for e in manifest["edges"]}
|
|
self.assertIn(
|
|
("article:concepts/transformer", "article:concepts/attention", "related"),
|
|
edge_keys,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|