structured tests
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user