From c653c7024b2acf7286998352ce9c93cd7e1bf407 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:36:44 -0500 Subject: [PATCH] structured tests --- .../catalog/test_prompt_loader_and_catalog.py | 131 ++++++++++++++++ .../catalog/test_step6_tool_fallback.py | 94 ++++++++++++ .../skills/test_document_loader_references.py | 144 ++++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 tests/integration/catalog/test_prompt_loader_and_catalog.py create mode 100644 tests/integration/catalog/test_step6_tool_fallback.py create mode 100644 tests/unit/skills/test_document_loader_references.py diff --git a/tests/integration/catalog/test_prompt_loader_and_catalog.py b/tests/integration/catalog/test_prompt_loader_and_catalog.py new file mode 100644 index 0000000..8fcb5ae --- /dev/null +++ b/tests/integration/catalog/test_prompt_loader_and_catalog.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from personal_mcp.catalog.server import ( + build_prompt_detail_payload, + build_prompts_index_payload, +) +from personal_mcp.skills import document_loader + + +def _write_skill(root: Path, *, skill_id: str) -> None: + skill_dir = root / "docs" / "skills" / skill_id + skill_dir.mkdir(parents=True, exist_ok=True) + skill_doc = "\n".join( + [ + "---", + f"name: {skill_id}", + "description: Example skill", + "x-personal-mcp:", + f" id: {skill_id}", + " version: 1.0.0", + " tags: [example]", + " capabilities:", + f" - resource://skills/{skill_id}/document", + "---", + "", + f"# {skill_id}", + "", + ] + ) + (skill_dir / "SKILL.md").write_text(skill_doc, encoding="utf-8") + + +def _write_frontmatter_prompt(root: Path, *, prompt_id: str) -> None: + prompt_dir = root / "docs" / "prompts" / prompt_id + prompt_dir.mkdir(parents=True, exist_ok=True) + prompt_doc = "\n".join( + [ + "---", + f"name: {prompt_id}", + "description: Scaffold initial tests", + "x-personal-mcp:", + f" id: {prompt_id}", + " version: 1.0.0", + " tags: [pytest, testing]", + " capabilities:", + f" - resource://prompts/{prompt_id}/document", + " arguments:", + " target_scope:", + " type: string", + " description: The test scope", + " required: true", + "---", + "", + "Create tests for {{target_scope}}.", + "", + ] + ) + (prompt_dir / "PROMPT.md").write_text(prompt_doc, encoding="utf-8") + + +def _write_legacy_prompt(root: Path) -> None: + legacy = root / "docs" / "prompts" / "testing" + legacy.mkdir(parents=True, exist_ok=True) + (legacy / "inital_test_structure.md").write_text( + "# Prompt For Creating Initial Test Structure\n", + encoding="utf-8", + ) + + +def _load_registry(monkeypatch: pytest.MonkeyPatch, root: Path): + monkeypatch.setattr(document_loader, "files", lambda _: root) + return document_loader.load_docs_registry(package_anchor="unused") + + +def test_loads_frontmatter_prompt_and_arguments(monkeypatch, tmp_path: Path) -> None: + _write_skill(tmp_path, skill_id="demo-skill") + _write_frontmatter_prompt(tmp_path, prompt_id="initial-test-structure") + + registry = _load_registry(monkeypatch, tmp_path) + + assert "initial-test-structure" in registry.prompts_by_id + prompt = registry.prompts_by_id["initial-test-structure"] + assert prompt.arguments["target_scope"].type == "string" + assert prompt.arguments["target_scope"].required is True + + +def test_loads_legacy_prompt_markdown(monkeypatch, tmp_path: Path) -> None: + _write_skill(tmp_path, skill_id="demo-skill") + _write_legacy_prompt(tmp_path) + + registry = _load_registry(monkeypatch, tmp_path) + + assert "inital-test-structure" in registry.prompts_by_id + prompt = registry.prompts_by_id["inital-test-structure"] + assert prompt.document_relpath == "prompts/testing/inital_test_structure.md" + assert "Legacy prompt loaded" in prompt.description + + +def test_rejects_prompt_skill_id_collision(monkeypatch, tmp_path: Path) -> None: + _write_skill(tmp_path, skill_id="shared-id") + _write_frontmatter_prompt(tmp_path, prompt_id="shared-id") + + with pytest.raises(document_loader.DocsRegistryValidationError) as exc: + _load_registry(monkeypatch, tmp_path) + + codes = [issue.code for issue in exc.value.errors] + assert "prompt_skill_id_collision" in codes + + +def test_prompt_catalog_index_and_detail(monkeypatch, tmp_path: Path) -> None: + _write_skill(tmp_path, skill_id="demo-skill") + _write_frontmatter_prompt(tmp_path, prompt_id="initial-test-structure") + _write_frontmatter_prompt(tmp_path, prompt_id="route-tests") + + registry = _load_registry(monkeypatch, tmp_path) + + index = build_prompts_index_payload( + registry, + query="initial-test-structure", + limit=10, + ) + assert index["total"] == 1 + assert index["prompts"][0]["id"] == "initial-test-structure" + + detail = build_prompt_detail_payload(registry, "initial-test-structure") + assert "target_scope" in detail["arguments"] + assert detail["arguments"]["target_scope"]["required"] is True diff --git a/tests/integration/catalog/test_step6_tool_fallback.py b/tests/integration/catalog/test_step6_tool_fallback.py new file mode 100644 index 0000000..2cd8bf3 --- /dev/null +++ b/tests/integration/catalog/test_step6_tool_fallback.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import asyncio + +import pytest + +from personal_mcp import mcp as mcp_module + + +def _listed_tool_names() -> set[str]: + tools = asyncio.run(mcp_module.mcp.list_tools()) + return {tool.name for tool in tools} + + +@pytest.fixture +def listed_tool_names() -> set[str]: + return _listed_tool_names() + + +def test_canonical_step6_tools_are_listed(listed_tool_names: set[str]) -> None: + assert { + "list_resources", + "read_resource", + "search_patterns", + "get_pattern_by_id", + "get_skill_document_by_id", + "search_prompts", + "get_prompt_by_id", + }.issubset(listed_tool_names) + + +def test_catalog_compatibility_alias_tools_are_listed( + listed_tool_names: set[str], +) -> None: + assert { + "catalog_search_patterns", + "catalog_get_pattern_by_id", + "catalog_get_skill_document_by_id", + "catalog_search_prompts", + "catalog_get_prompt_by_id", + }.issubset(listed_tool_names) + + +@pytest.mark.parametrize( + "skill_id", + [mcp_module.REGISTRY.skills_in_load_order[0], "missing-step6-skill"], +) +def test_skill_document_alias_matches_canonical_payload(skill_id: str) -> None: + canonical = mcp_module.get_skill_document_by_id(skill_id) + alias = mcp_module.catalog_get_skill_document_by_id(skill_id) + + assert alias == canonical + + +def test_search_and_pattern_aliases_match_canonical_payloads() -> None: + search_canonical = mcp_module.search_patterns( + query="mcp", + tags=[], + skip=0, + limit=5, + ) + search_alias = mcp_module.catalog_search_patterns( + query="mcp", + tags=[], + skip=0, + limit=5, + ) + assert search_alias == search_canonical + + sample_skill_id = mcp_module.REGISTRY.skills_in_load_order[0] + pattern_canonical = mcp_module.get_pattern_by_id(sample_skill_id) + pattern_alias = mcp_module.catalog_get_pattern_by_id(sample_skill_id) + assert pattern_alias == pattern_canonical + + +def test_prompt_aliases_match_canonical_payloads() -> None: + search_canonical = mcp_module.search_prompts( + query="test", + tags=[], + skip=0, + limit=5, + ) + search_alias = mcp_module.catalog_search_prompts( + query="test", + tags=[], + skip=0, + limit=5, + ) + assert search_alias == search_canonical + + sample_prompt_id = mcp_module.REGISTRY.prompts_in_load_order[0] + prompt_canonical = mcp_module.get_prompt_by_id(sample_prompt_id) + prompt_alias = mcp_module.catalog_get_prompt_by_id(sample_prompt_id) + assert prompt_alias == prompt_canonical diff --git a/tests/unit/skills/test_document_loader_references.py b/tests/unit/skills/test_document_loader_references.py new file mode 100644 index 0000000..9e92496 --- /dev/null +++ b/tests/unit/skills/test_document_loader_references.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from personal_mcp.skills import document_loader + + +def _write_skill( + root: Path, + *, + skill_id: str, + references_block: str = "", +) -> Path: + skill_dir = root / "docs" / "skills" / skill_id + skill_dir.mkdir(parents=True, exist_ok=True) + frontmatter_lines = [ + "---", + f"name: {skill_id}", + "description: Example skill", + "x-personal-mcp:", + f" id: {skill_id}", + " version: 1.0.0", + " tags: [example]", + " capabilities:", + f" - resource://skills/{skill_id}/document", + ] + if references_block: + frontmatter_lines.extend(f" {line}" for line in references_block.splitlines()) + frontmatter_lines.append("---") + + skill_doc = "\n".join(frontmatter_lines) + f"\n\n# {skill_id}\n" + (skill_dir / "SKILL.md").write_text(skill_doc, encoding="utf-8") + return skill_dir + + +def _load_registry(monkeypatch: pytest.MonkeyPatch, root: Path): + monkeypatch.setattr(document_loader, "files", lambda _: root) + return document_loader.load_docs_registry(package_anchor="unused") + + +def test_auto_discovers_top_level_references(monkeypatch, tmp_path: Path) -> None: + skill_dir = _write_skill(tmp_path, skill_id="demo-skill") + refs = skill_dir / "references" + refs.mkdir() + (refs / "index.md").write_text("# Source Map\n", encoding="utf-8") + (refs / "feature-catalog.md").write_text("# Feature Catalog\n", encoding="utf-8") + + registry = _load_registry(monkeypatch, tmp_path) + skill = registry.skills_by_id["demo-skill"] + + assert set(skill.references) == {"index", "feature-catalog"} + assert skill.references["index"].title == "Index" + assert skill.references["feature-catalog"].title == "Feature Catalog" + + +def test_explicit_reference_overrides_discovered(monkeypatch, tmp_path: Path) -> None: + references_block = dedent( + """\ + references: + index: + path: references/index.md + mime_type: text/plain + title: Explicit Title + """ + ).rstrip() + skill_dir = _write_skill( + tmp_path, + skill_id="override-skill", + references_block=references_block, + ) + refs = skill_dir / "references" + refs.mkdir() + (refs / "index.md").write_text("# Source Map\n", encoding="utf-8") + + registry = _load_registry(monkeypatch, tmp_path) + record = registry.skills_by_id["override-skill"].references["index"] + + assert record.mime_type == "text/plain" + assert record.title == "Explicit Title" + + +def test_nested_references_are_not_auto_discovered(monkeypatch, tmp_path: Path) -> None: + references_block = dedent( + """\ + references: + architecture-overview: + path: references/nested/architecture-overview.md + title: Architecture Overview + """ + ).rstrip() + skill_dir = _write_skill( + tmp_path, + skill_id="nested-skill", + references_block=references_block, + ) + refs = skill_dir / "references" + (refs / "nested").mkdir(parents=True) + (refs / "nested" / "architecture-overview.md").write_text( + "# Architecture\n", + encoding="utf-8", + ) + + registry = _load_registry(monkeypatch, tmp_path) + skill = registry.skills_by_id["nested-skill"] + + assert set(skill.references) == {"architecture-overview"} + + +@pytest.mark.parametrize( + ("skill_id", "files", "expected_refs"), + [ + ( + "non-markdown-skill", + {"guide.txt": "plain text", "guide.md": "# Guide\n"}, + {"guide"}, + ), + ( + "normalized-id-skill", + {"implicit_io.md": "# Implicit IO\n"}, + {"implicit-io"}, + ), + ], +) +def test_auto_discovery_file_filtering_and_ref_id_normalization( + monkeypatch, + tmp_path: Path, + skill_id: str, + files: dict[str, str], + expected_refs: set[str], +) -> None: + skill_dir = _write_skill(tmp_path, skill_id=skill_id) + refs = skill_dir / "references" + refs.mkdir() + + for filename, content in files.items(): + (refs / filename).write_text(content, encoding="utf-8") + + registry = _load_registry(monkeypatch, tmp_path) + skill = registry.skills_by_id[skill_id] + + assert set(skill.references) == expected_refs