Files
AegisGitea-MCP/tests/test_check_docs.py
T
2026-10-14 16:13:11 +02:00

240 lines
9.7 KiB
Python

"""Tests for scripts/check_docs.py: generation, drift detection and tool name mentions."""
from __future__ import annotations
import importlib.util
import re
import sys
from pathlib import Path
from types import ModuleType
import pytest
from aegis_gitea_mcp.catalog import load_catalog
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "scripts" / "check_docs.py"
README_TEMPLATE = (
"# Title\n\nIntro.\n\n<!-- tool-summary:start -->\nold\n<!-- tool-summary:end -->\n\nOutro.\n"
)
@pytest.fixture(scope="module")
def script() -> ModuleType:
spec = importlib.util.spec_from_file_location("check_docs_script", SCRIPT)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module # dataclasses resolves string annotations through it
spec.loader.exec_module(module)
return module
@pytest.fixture
def tree(script: ModuleType, tmp_path: Path) -> Path:
"""A minimal docs tree in which the generated files are up to date."""
(tmp_path / "docs").mkdir()
(tmp_path / "README.md").write_text(README_TEMPLATE, encoding="utf-8")
script.write(tmp_path)
return tmp_path
def test_repository_docs_are_in_sync(script: ModuleType) -> None:
# docs/security.md still describes the 0.x design until the security phase rewrites it;
# its stale tool names are the only accepted problems. Drop the filter after that rewrite.
problems = [p for p in script.check(REPO_ROOT) if not p.startswith("docs/security.md:")]
assert problems == []
def test_tools_md_lists_every_tool_once(script: ModuleType) -> None:
catalog = load_catalog()
text = script.render_tools_md(catalog)
rows = re.findall(r"^\| `(gitea_[a-z0-9_]+)` \|", text, flags=re.MULTILINE)
expected = {op.tool_name for op in catalog.operations} | {"gitea_api_request"}
assert len(rows) == len(expected) == len(catalog.operations) + 1
assert set(rows) == expected
assert f"There are {len(expected)} tools" in text
assert catalog.spec_version in text
def test_tools_md_row_content(script: ModuleType) -> None:
catalog = load_catalog()
text = script.render_tools_md(catalog)
delete = next(op for op in catalog.operations if op.tool_name == "gitea_repo_delete")
row = next(line for line in text.splitlines() if line.startswith("| `gitea_repo_delete` |"))
assert f"| {delete.method} | `{delete.path}` |" in row
assert "destructive" in row
raw_row = next(line for line in text.splitlines() if line.startswith("| `gitea_api_request`"))
assert "any" in raw_row
assert "destructive" in raw_row
def test_flags_cover_admin_upload_and_pagination(script: ModuleType) -> None:
catalog = load_catalog()
rows = {row.name: row for row in script.build_rows(catalog)}
admin = next(op.tool_name for op in catalog.operations if op.domain == "admin")
assert "admin" in rows[admin].flags
assert any("file upload" in row.flags for row in rows.values())
assert any("paginated" in row.flags for row in rows.values())
assert any("read-only" in row.flags for row in rows.values())
def test_output_is_deterministic(script: ModuleType) -> None:
catalog = load_catalog()
assert script.render_tools_md(catalog) == script.render_tools_md(catalog)
assert script.render_readme_block(catalog) == script.render_readme_block(catalog)
def test_summary_table_counts_match_catalog(script: ModuleType) -> None:
catalog = load_catalog()
block = script.render_readme_block(catalog)
total = len(catalog.operations) + 1
assert f"{total} tools" in block
misc = sum(1 for op in catalog.operations if op.domain == "misc") + 1
assert f"| misc | {misc} |" in block
assert f"| **all** | **{total}** |" in block
def test_table_cells_escape_pipes_and_newlines(script: ModuleType) -> None:
assert script._cell("a | b\nc") == "a \\| b c"
def test_write_mode_creates_files_and_replaces_only_the_block(
script: ModuleType, tmp_path: Path
) -> None:
(tmp_path / "README.md").write_text(README_TEMPLATE, encoding="utf-8")
written = script.write(tmp_path)
assert {path.name for path in written} == {"tools.md", "README.md"}
readme = (tmp_path / "README.md").read_text(encoding="utf-8")
assert readme.startswith("# Title\n\nIntro.\n\n<!-- tool-summary:start -->")
assert readme.endswith("<!-- tool-summary:end -->\n\nOutro.\n")
assert "old" not in readme
assert (tmp_path / "docs" / "tools.md").read_text(encoding="utf-8").startswith("# Tool")
def test_write_mode_is_idempotent(script: ModuleType, tree: Path) -> None:
before = (tree / "README.md").read_text(encoding="utf-8")
script.write(tree)
assert (tree / "README.md").read_text(encoding="utf-8") == before
def test_write_requires_markers(script: ModuleType, tmp_path: Path) -> None:
(tmp_path / "README.md").write_text("# no markers\n", encoding="utf-8")
with pytest.raises(ValueError, match="tool-summary"):
script.write(tmp_path)
assert script.main(["--write"], root=tmp_path) == 1
def test_main_write_then_check(
script: ModuleType, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
(tmp_path / "README.md").write_text(README_TEMPLATE, encoding="utf-8")
assert script.main(["--write"], root=tmp_path) == 0
assert "wrote docs/tools.md" in capsys.readouterr().out
assert script.main([], root=tmp_path) == 0
assert "docs check passed" in capsys.readouterr().out
def test_clean_tree_passes(script: ModuleType, tree: Path) -> None:
assert script.check(tree) == []
def test_detects_stale_tools_md(
script: ModuleType, tree: Path, capsys: pytest.CaptureFixture[str]
) -> None:
path = tree / "docs" / "tools.md"
path.write_text(path.read_text(encoding="utf-8").replace("| repo |", "| repos |"))
problems = script.check(tree)
assert len(problems) == 1
assert "docs/tools.md is out of date" in problems[0]
assert "check_docs.py --write" in problems[0]
assert "-| repos |" in problems[0]
assert "+| repo |" in problems[0]
assert script.main([], root=tree) == 1
assert "docs check failed" in capsys.readouterr().err
def test_detects_missing_tools_md(script: ModuleType, tree: Path) -> None:
(tree / "docs" / "tools.md").unlink()
problems = script.check(tree)
assert any("docs/tools.md is missing" in problem for problem in problems)
def test_detects_stale_readme_block(script: ModuleType, tree: Path) -> None:
readme = tree / "README.md"
readme.write_text(readme.read_text(encoding="utf-8").replace("| misc |", "| miscx |"))
problems = script.check(tree)
assert len(problems) == 1
assert "tool summary in README.md is out of date" in problems[0]
def test_detects_missing_readme_markers(script: ModuleType, tree: Path) -> None:
(tree / "README.md").write_text("# no block here\n", encoding="utf-8")
problems = script.check(tree)
assert any("no generated block" in problem for problem in problems)
def test_detects_reversed_markers(script: ModuleType, tree: Path) -> None:
(tree / "README.md").write_text(
"<!-- tool-summary:end -->\n<!-- tool-summary:start -->\n", encoding="utf-8"
)
assert any("no generated block" in problem for problem in script.check(tree))
def test_detects_unknown_tool_name_in_readme(script: ModuleType, tree: Path) -> None:
readme = tree / "README.md"
readme.write_text(readme.read_text(encoding="utf-8") + "\nUse `gitea_made_up_tool`.\n")
problems = script.check(tree)
assert len(problems) == 1
assert "README.md" in problems[0]
assert "'gitea_made_up_tool' is not a registered tool name" in problems[0]
def test_detects_unknown_tool_name_in_docs(script: ModuleType, tree: Path) -> None:
(tree / "docs" / "guide.md").write_text("one\ntwo `gitea_request` three\n", encoding="utf-8")
problems = script.check(tree)
assert problems == ["docs/guide.md:2: 'gitea_request' is not a registered tool name"]
def test_registered_names_and_prefixes_are_accepted(script: ModuleType, tree: Path) -> None:
(tree / "docs" / "guide.md").write_text(
"`gitea_repo_get`, `gitea_api_request` and the gitea_admin_ tools.\n", encoding="utf-8"
)
assert script.check(tree) == []
def test_unknown_prefix_is_rejected(script: ModuleType, tree: Path) -> None:
(tree / "docs" / "guide.md").write_text("the gitea_bogus_ tools\n", encoding="utf-8")
assert len(script.check(tree)) == 1
def test_similar_identifiers_are_not_tool_mentions(script: ModuleType) -> None:
names = script.registered_names(load_catalog())
text = "aegis_gitea_mcp, my_gitea_thing, GITEA_URL and gitea_<domain>_<action>\n"
assert script.find_unknown_mentions(text, names) == []
def test_excluded_documents_may_name_old_tools(script: ModuleType, tree: Path) -> None:
(tree / "docs" / "audit.md").write_text("old `gitea_request`\n", encoding="utf-8")
(tree / "docs" / "adr").mkdir()
(tree / "docs" / "adr" / "0001.md").write_text("old `gitea_request`\n", encoding="utf-8")
(tree / "docs" / "research").mkdir()
(tree / "docs" / "research" / "notes.md").write_text("`gitea_request`\n", encoding="utf-8")
assert script.check(tree) == []
scanned = {path.name for path in script.doc_files(tree)}
assert scanned == {"README.md", "tools.md"}
def test_missing_readme_is_reported(script: ModuleType, tree: Path) -> None:
(tree / "README.md").unlink()
assert any("README.md is missing" in problem for problem in script.check(tree))
def test_long_diffs_are_truncated(script: ModuleType) -> None:
current = "\n".join(f"old {i}" for i in range(200))
expected = "\n".join(f"new {i}" for i in range(200))
summary = script._diff_summary("x", current, expected)
assert "more diff lines" in summary
assert len(summary.splitlines()) <= 30