Files
2026-07-13 12:42:18 +08:00

255 lines
9.7 KiB
Python

"""Tests for wiki generation."""
import tempfile
from pathlib import Path
from code_review_graph.communities import detect_communities, store_communities
from code_review_graph.graph import GraphStore
from code_review_graph.parser import EdgeInfo, NodeInfo
from code_review_graph.wiki import (
_generate_community_page,
_slugify,
generate_wiki,
get_wiki_page,
)
class TestWiki:
def setup_method(self):
self.tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
self.store = GraphStore(self.tmp.name)
self.wiki_dir = tempfile.mkdtemp()
def teardown_method(self):
self.store.close()
Path(self.tmp.name).unlink(missing_ok=True)
# Clean up wiki dir
wiki_path = Path(self.wiki_dir)
if wiki_path.exists():
for f in wiki_path.iterdir():
f.unlink(missing_ok=True)
wiki_path.rmdir()
def _seed_communities(self):
"""Seed graph data and detect/store communities."""
# Auth cluster
self.store.upsert_node(
NodeInfo(
kind="File", name="auth.py", file_path="auth.py",
line_start=1, line_end=100, language="python",
), file_hash="a1"
)
self.store.upsert_node(
NodeInfo(
kind="Function", name="login", file_path="auth.py",
line_start=5, line_end=20, language="python",
), file_hash="a1"
)
self.store.upsert_node(
NodeInfo(
kind="Function", name="logout", file_path="auth.py",
line_start=25, line_end=40, language="python",
), file_hash="a1"
)
self.store.upsert_node(
NodeInfo(
kind="Function", name="check_token", file_path="auth.py",
line_start=45, line_end=60, language="python",
), file_hash="a1"
)
self.store.upsert_edge(EdgeInfo(
kind="CALLS", source="auth.py::login",
target="auth.py::check_token", file_path="auth.py", line=10,
))
self.store.upsert_edge(EdgeInfo(
kind="CALLS", source="auth.py::logout",
target="auth.py::check_token", file_path="auth.py", line=30,
))
# DB cluster
self.store.upsert_node(
NodeInfo(
kind="File", name="db.py", file_path="db.py",
line_start=1, line_end=100, language="python",
), file_hash="b1"
)
self.store.upsert_node(
NodeInfo(
kind="Function", name="connect", file_path="db.py",
line_start=5, line_end=20, language="python",
), file_hash="b1"
)
self.store.upsert_node(
NodeInfo(
kind="Function", name="query", file_path="db.py",
line_start=25, line_end=40, language="python",
), file_hash="b1"
)
self.store.upsert_edge(EdgeInfo(
kind="CALLS", source="db.py::query",
target="db.py::connect", file_path="db.py", line=30,
))
self.store.commit()
communities = detect_communities(self.store, min_size=2)
store_communities(self.store, communities)
return communities
def test_generate_wiki_creates_files(self):
"""generate_wiki creates markdown files including index.md."""
self._seed_communities()
result = generate_wiki(self.store, self.wiki_dir)
wiki_path = Path(self.wiki_dir)
assert (wiki_path / "index.md").exists()
# At least one community page should be generated
md_files = list(wiki_path.glob("*.md"))
assert len(md_files) >= 2 # index + at least 1 community page
assert result["pages_generated"] >= 2
assert isinstance(result["pages_updated"], int)
assert isinstance(result["pages_unchanged"], int)
def test_generate_wiki_index_has_links(self):
"""index.md contains links to community pages."""
self._seed_communities()
generate_wiki(self.store, self.wiki_dir)
index_content = (Path(self.wiki_dir) / "index.md").read_text()
assert "# Code Wiki" in index_content
assert "Communities" in index_content
assert ".md" in index_content # contains links to .md files
def test_get_wiki_page_returns_content(self):
"""get_wiki_page returns content for an existing page."""
self._seed_communities()
generate_wiki(self.store, self.wiki_dir)
# Find any generated page
wiki_path = Path(self.wiki_dir)
pages = [f for f in wiki_path.glob("*.md") if f.name != "index.md"]
assert len(pages) > 0
# Get page by its stem (slug)
page_name = pages[0].stem
content = get_wiki_page(self.wiki_dir, page_name)
assert content is not None
assert len(content) > 0
def test_get_wiki_page_returns_none_for_missing(self):
"""get_wiki_page returns None for non-existent page."""
content = get_wiki_page(self.wiki_dir, "nonexistent-page")
assert content is None
def test_community_page_has_expected_sections(self):
"""Generated community pages contain expected sections."""
communities = self._seed_communities()
assert len(communities) > 0
from code_review_graph.communities import get_communities
stored = get_communities(self.store)
assert len(stored) > 0
page = _generate_community_page(self.store, stored[0])
assert "## Overview" in page
assert "## Members" in page
assert "## Execution Flows" in page
assert "## Dependencies" in page
def test_slugify(self):
"""_slugify converts names to safe filenames."""
assert _slugify("auth-login") == "auth-login"
assert _slugify("My Community Name") == "my-community-name"
assert _slugify("") == "unnamed"
assert _slugify("auth/sub-cluster") == "auth-sub-cluster"
def test_generate_wiki_force_regenerates(self):
"""generate_wiki with force=True regenerates all pages."""
self._seed_communities()
# First generation
result1 = generate_wiki(self.store, self.wiki_dir)
assert result1["pages_generated"] >= 2
# Second generation without force - should be unchanged
result2 = generate_wiki(self.store, self.wiki_dir)
assert result2["pages_unchanged"] >= 1
# Third generation with force - should update all
result3 = generate_wiki(self.store, self.wiki_dir, force=True)
assert result3["pages_generated"] + result3["pages_updated"] >= 1
def test_generate_wiki_empty_graph(self):
"""generate_wiki on empty graph creates index with no communities."""
result = generate_wiki(self.store, self.wiki_dir)
assert result["pages_generated"] >= 1 # at least index.md
index_content = (Path(self.wiki_dir) / "index.md").read_text()
assert "Total communities" in index_content
assert "0" in index_content # 0 communities
def test_generate_wiki_handles_slug_collisions(self, monkeypatch):
"""Communities whose names slugify to the same string must each get
their own page — earlier behaviour silently overwrote the first
community's file with the second's content and counted the second
as an 'updated' page (see #222 follow-up).
"""
# Fake three communities whose _slugify outputs collide:
# "Data Processing" -> data-processing
# "data processing" -> data-processing
# "Data Processing" -> data-processing
colliding_communities = [
{
"name": "Data Processing", "size": 5, "cohesion": 0.9,
"dominant_language": "python", "description": "first",
"members": [], "member_qns": set(),
},
{
"name": "data processing", "size": 4, "cohesion": 0.8,
"dominant_language": "python", "description": "second",
"members": [], "member_qns": set(),
},
{
"name": "Data Processing", "size": 3, "cohesion": 0.7,
"dominant_language": "python", "description": "third",
"members": [], "member_qns": set(),
},
]
import code_review_graph.wiki as wiki_mod
monkeypatch.setattr(
wiki_mod, "get_communities", lambda store: colliding_communities,
)
result = generate_wiki(self.store, self.wiki_dir)
# 3 unique .md pages + 1 index.md should land on disk.
wiki_files = sorted(p.name for p in Path(self.wiki_dir).glob("*.md"))
expected = {
"data-processing.md",
"data-processing-2.md",
"data-processing-3.md",
"index.md",
}
assert set(wiki_files) == expected, wiki_files
# Counter must match what actually hit the disk.
page_total = (
result["pages_generated"]
+ result["pages_updated"]
+ result["pages_unchanged"]
)
assert page_total == len(wiki_files), (
f"counter {result} but {len(wiki_files)} files on disk"
)
# Every community's description must survive (no data loss).
content_first = (Path(self.wiki_dir) / "data-processing.md").read_text()
content_second = (Path(self.wiki_dir) / "data-processing-2.md").read_text()
content_third = (Path(self.wiki_dir) / "data-processing-3.md").read_text()
# Descriptions are rendered in the page body; make sure all three
# communities produced distinct content.
assert content_first != content_second
assert content_first != content_third
assert content_second != content_third