migration
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class TestPromptContentRenderer:
|
||||
def test_renders_arguments_without_frontmatter(self) -> None:
|
||||
rendered = render_prompt(
|
||||
"jsfiddle-page-layout",
|
||||
{"domain": "public library", "layout_brief": None},
|
||||
)
|
||||
|
||||
assert not rendered.startswith("---")
|
||||
assert "`domain`: public library" in rendered
|
||||
assert "`layout_brief`: Not provided" in rendered
|
||||
|
||||
def test_rejects_placeholder_drift(self) -> None:
|
||||
with pytest.raises(ValueError, match="placeholders do not match arguments"):
|
||||
render_prompt("jsfiddle-page-layout", {"domain": "public library"})
|
||||
|
||||
def test_rejects_invalid_prompt_id(self) -> None:
|
||||
with pytest.raises(ValueError, match="lowercase kebab-case"):
|
||||
render_prompt("../outside", {})
|
||||
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.prompts import create_prompts_provider
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
EXPECTED_PROMPTS = {
|
||||
"authoring",
|
||||
"greenfield-architecture",
|
||||
"jsfiddle-page-layout",
|
||||
"mcp-consumer-repo-shim",
|
||||
"nicegui-component-extraction",
|
||||
"pytest-fill-scaffold",
|
||||
"pytest-scaffold",
|
||||
}
|
||||
|
||||
|
||||
class TestPromptFileSystemProvider:
|
||||
@pytest.mark.asyncio
|
||||
async def test_discovers_exact_authored_set(self) -> None:
|
||||
mcp = FastMCP("prompts-test")
|
||||
mcp.add_provider(create_prompts_provider())
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
|
||||
assert {prompt.name for prompt in prompts} == EXPECTED_PROMPTS
|
||||
assert all(prompt.description for prompt in prompts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exposes_typed_arguments_and_renders_markdown(self) -> None:
|
||||
mcp = FastMCP("prompts-test")
|
||||
mcp.add_provider(create_prompts_provider())
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
authoring = next(prompt for prompt in prompts if prompt.name == "authoring")
|
||||
result = await client.get_prompt(
|
||||
"authoring",
|
||||
{
|
||||
"artifact_type": "skill",
|
||||
"artifact_id": "demo-skill",
|
||||
"goal": "Demonstrate typed prompts.",
|
||||
},
|
||||
)
|
||||
|
||||
required = {argument.name for argument in authoring.arguments or [] if argument.required}
|
||||
assert required == {"artifact_type", "artifact_id", "goal"}
|
||||
assert result.messages
|
||||
assert "`artifact_id`: demo-skill" in result.messages[0].content.text
|
||||
@@ -1,18 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.ingest.document import MarkdownDocument
|
||||
|
||||
# Ingest-specific fixtures and factories belong in this subtree conftest.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_doc() -> Callable[[str], MarkdownDocument]:
|
||||
def _make_doc(relpath: str, content: str = "# body\n") -> MarkdownDocument:
|
||||
return MarkdownDocument(relpath=PurePosixPath(relpath), content=content)
|
||||
|
||||
return _make_doc
|
||||
@@ -1,32 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.ingest.document import MarkdownDocument
|
||||
from personal_mcp.registry.ingest.prompt import PromptFilesBundle
|
||||
from personal_mcp.registry.load import get_docs_registry
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
REPO_ROOT = Path(__file__).parents[3]
|
||||
DOCS_ROOT = REPO_ROOT / "docs"
|
||||
|
||||
|
||||
class TestCurrentDocsIngestion:
|
||||
"""Covers ingestion of the repository's current docs tree."""
|
||||
|
||||
def test_loads_markdown_documents(self) -> None:
|
||||
"""Ensures all current markdown documents can be loaded."""
|
||||
docs = MarkdownDocument.from_root(DOCS_ROOT)
|
||||
def test_registry_includes_docs_and_excludes_skills(self) -> None:
|
||||
"""Ensures the docs registry cannot duplicate native skill resources."""
|
||||
registry = get_docs_registry()
|
||||
|
||||
assert docs
|
||||
|
||||
def test_bundles_current_prompts(self) -> None:
|
||||
"""Ensures all current canonical prompt documents can be bundled."""
|
||||
docs = MarkdownDocument.from_root(DOCS_ROOT)
|
||||
expected_slugs = {path.parent.name for path in DOCS_ROOT.glob("prompts/*/PROMPT.md")}
|
||||
|
||||
bundles = PromptFilesBundle.from_docs(docs.values())
|
||||
|
||||
assert {bundle.slug for bundle in bundles} == expected_slugs
|
||||
assert PurePosixPath("index.md") in registry.docs_markdown_by_path
|
||||
assert any(path.parts[0] == "prompts" for path in registry.docs_markdown_by_path)
|
||||
assert all(path.parts[0] != "skills" for path in registry.docs_markdown_by_path)
|
||||
|
||||
@@ -1,176 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.ingest.document import MarkdownDocument
|
||||
from personal_mcp.registry.ingest.document import get_frontmatter_delim_idx
|
||||
from personal_mcp.registry.ingest.document import get_raw_frontmatter
|
||||
from personal_mcp.registry.ingest.document import walk_resources
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class TestMarkdownDocument:
|
||||
"""Covers MarkdownDocument construction and derived properties."""
|
||||
"""Covers recursive Markdown discovery and loading."""
|
||||
|
||||
class TestFromRoot:
|
||||
"""Covers loading markdown documents from a resource root."""
|
||||
def test_loads_markdown_in_stable_order(self, tmp_path: Path) -> None:
|
||||
nested = tmp_path / "guides"
|
||||
nested.mkdir()
|
||||
(nested / "b.md").write_text("caf\u00e9\n", encoding="utf-8")
|
||||
(nested / "a.md").write_text("alpha\n", encoding="utf-8")
|
||||
(nested / "ignored.txt").write_text("ignored\n", encoding="utf-8")
|
||||
|
||||
def test_keys_by_relpath(self, tmp_path: Path) -> None:
|
||||
"""Ensures from_root returns a mapping keyed by relative path."""
|
||||
skills_dir = tmp_path / "skills" / "demo"
|
||||
skills_dir.mkdir(parents=True)
|
||||
(skills_dir / "SKILL.md").write_text("# demo\n", encoding="utf-8")
|
||||
docs = MarkdownDocument.from_root(tmp_path)
|
||||
|
||||
docs = MarkdownDocument.from_root(tmp_path)
|
||||
|
||||
assert set(docs) == {PurePosixPath("skills/demo/SKILL.md")}
|
||||
|
||||
def test_loads_markdown_only(self, tmp_path: Path) -> None:
|
||||
"""Ensures from_root includes only markdown resources."""
|
||||
(tmp_path / "a.md").write_text("a\n", encoding="utf-8")
|
||||
(tmp_path / "b.MD").write_text("b\n", encoding="utf-8")
|
||||
(tmp_path / "c.txt").write_text("c\n", encoding="utf-8")
|
||||
|
||||
docs = MarkdownDocument.from_root(tmp_path)
|
||||
|
||||
assert set(docs) == {PurePosixPath("a.md"), PurePosixPath("b.MD")}
|
||||
|
||||
def test_preserves_relpaths(self, tmp_path: Path) -> None:
|
||||
"""Ensures from_root preserves PurePosixPath-style relative paths."""
|
||||
nested = tmp_path / "skills" / "slug"
|
||||
nested.mkdir(parents=True)
|
||||
(nested / "SKILL.md").write_text("# slug\n", encoding="utf-8")
|
||||
|
||||
docs = MarkdownDocument.from_root(tmp_path)
|
||||
[relpath] = docs.keys()
|
||||
|
||||
assert isinstance(relpath, PurePosixPath)
|
||||
assert relpath == PurePosixPath("skills/slug/SKILL.md")
|
||||
|
||||
class TestFromResource:
|
||||
"""Covers loading a single markdown document from a resource."""
|
||||
|
||||
def test_reads_utf8(self, tmp_path: Path) -> None:
|
||||
"""Ensures from_resource reads text using UTF-8."""
|
||||
resource = tmp_path / "index.md"
|
||||
resource.write_text("caf\u00e9\n", encoding="utf-8")
|
||||
|
||||
doc = MarkdownDocument.from_resource(PurePosixPath("index.md"), resource)
|
||||
|
||||
assert doc.content == "caf\u00e9\n"
|
||||
|
||||
def test_sets_frontmatter(self, tmp_path: Path) -> None:
|
||||
"""Ensures from_resource stores frontmatter when delimiters exist."""
|
||||
resource = tmp_path / "index.md"
|
||||
resource.write_text("---\nname: demo\n---\n# body\n", encoding="utf-8")
|
||||
|
||||
doc = MarkdownDocument.from_resource(PurePosixPath("index.md"), resource)
|
||||
|
||||
assert doc.frontmatter == "name: demo"
|
||||
|
||||
def test_none_frontmatter(self, tmp_path: Path) -> None:
|
||||
"""Ensures from_resource sets frontmatter to None when absent."""
|
||||
resource = tmp_path / "index.md"
|
||||
resource.write_text("# no frontmatter\n", encoding="utf-8")
|
||||
|
||||
doc = MarkdownDocument.from_resource(PurePosixPath("index.md"), resource)
|
||||
|
||||
assert doc.frontmatter is None
|
||||
|
||||
class TestPromptSlugProperty:
|
||||
"""Covers prompt_slug derivation from document relative paths."""
|
||||
|
||||
def test_returns_slug(self) -> None:
|
||||
"""Ensures prompt_slug returns the slug for valid prompt paths."""
|
||||
doc = MarkdownDocument(relpath=PurePosixPath("prompts/demo/PROMPT.md"), content="#")
|
||||
|
||||
assert doc.prompt_slug == "demo"
|
||||
|
||||
def test_none_for_non_prompt(self) -> None:
|
||||
"""Ensures prompt_slug is None for non-prompt paths."""
|
||||
doc = MarkdownDocument(relpath=PurePosixPath("docs/index.md"), content="#")
|
||||
|
||||
assert doc.prompt_slug is None
|
||||
|
||||
def test_none_for_incomplete_prompt(self) -> None:
|
||||
"""Ensures prompt_slug is None for incomplete prompt paths."""
|
||||
doc = MarkdownDocument(relpath=PurePosixPath("prompts/demo.md"), content="#")
|
||||
|
||||
assert doc.prompt_slug is None
|
||||
|
||||
|
||||
class TestWalkResources:
|
||||
"""Covers recursive resource walking and markdown filtering behavior."""
|
||||
|
||||
def test_yields_markdown(self, tmp_path: Path) -> None:
|
||||
"""Ensures walk_resources yields markdown files from nested directories."""
|
||||
nested = tmp_path / "skills" / "alpha"
|
||||
nested.mkdir(parents=True)
|
||||
(nested / "SKILL.md").write_text("# alpha\n", encoding="utf-8")
|
||||
(tmp_path / "index.md").write_text("# index\n", encoding="utf-8")
|
||||
|
||||
relpaths = [path for path, _ in walk_resources(tmp_path)]
|
||||
|
||||
assert relpaths == [
|
||||
PurePosixPath("index.md"),
|
||||
PurePosixPath("skills/alpha/SKILL.md"),
|
||||
assert list(docs) == [
|
||||
PurePosixPath("guides/a.md"),
|
||||
PurePosixPath("guides/b.md"),
|
||||
]
|
||||
|
||||
def test_ignores_other_suffixes(self, tmp_path: Path) -> None:
|
||||
"""Ensures walk_resources excludes files with non-matching suffixes."""
|
||||
(tmp_path / "a.md").write_text("a\n", encoding="utf-8")
|
||||
(tmp_path / "b.txt").write_text("b\n", encoding="utf-8")
|
||||
(tmp_path / "c.json").write_text("c\n", encoding="utf-8")
|
||||
|
||||
relpaths = [path for path, _ in walk_resources(tmp_path)]
|
||||
|
||||
assert relpaths == [PurePosixPath("a.md")]
|
||||
|
||||
def test_sorted_output(self, tmp_path: Path) -> None:
|
||||
"""Ensures walk_resources yields entries in sorted child-name order."""
|
||||
(tmp_path / "b.md").write_text("b\n", encoding="utf-8")
|
||||
(tmp_path / "a.md").write_text("a\n", encoding="utf-8")
|
||||
(tmp_path / "skills").mkdir()
|
||||
(tmp_path / "skills" / "z.md").write_text("z\n", encoding="utf-8")
|
||||
|
||||
relpaths = [path for path, _ in walk_resources(tmp_path)]
|
||||
|
||||
assert relpaths == [
|
||||
PurePosixPath("a.md"),
|
||||
PurePosixPath("b.md"),
|
||||
PurePosixPath("skills/z.md"),
|
||||
]
|
||||
|
||||
def test_applies_prefix(self, tmp_path: Path) -> None:
|
||||
"""Ensures walk_resources prepends the provided prefix to relpaths."""
|
||||
(tmp_path / "index.md").write_text("# index\n", encoding="utf-8")
|
||||
|
||||
relpaths = [path for path, _ in walk_resources(tmp_path, prefix=PurePosixPath("docs"))]
|
||||
|
||||
assert relpaths == [PurePosixPath("docs/index.md")]
|
||||
|
||||
|
||||
class TestFrontmatterParsing:
|
||||
"""Covers frontmatter delimiter discovery and raw block extraction."""
|
||||
|
||||
def test_extracts_between_delimiters(self) -> None:
|
||||
"""Ensures get_raw_frontmatter returns lines between first delimiters."""
|
||||
raw = "---\nname: demo\ntags:\n - test\n---\n# body\n"
|
||||
|
||||
assert get_raw_frontmatter(raw) == "name: demo\ntags:\n - test"
|
||||
|
||||
def test_none_without_two_delimiters(self) -> None:
|
||||
"""Ensures get_raw_frontmatter returns None without two delimiters."""
|
||||
raw = "---\nname: demo\n# body\n"
|
||||
|
||||
assert get_raw_frontmatter(raw) is None
|
||||
|
||||
def test_allows_leading_whitespace(self) -> None:
|
||||
"""Ensures delimiter detection accepts lines with leading whitespace."""
|
||||
raw = " ---\nname: demo\n ---\n# body\n"
|
||||
|
||||
assert list(get_frontmatter_delim_idx(raw)) == [0, 2]
|
||||
assert docs[PurePosixPath("guides/b.md")].content == "caf\u00e9\n"
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.ingest.document import MarkdownDocument
|
||||
from personal_mcp.registry.ingest.prompt import PromptFilesBundle
|
||||
from personal_mcp.registry.ingest.prompt import group_prompt_paths
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
MakeDoc = Callable[[str], MarkdownDocument]
|
||||
|
||||
|
||||
class TestPromptFilesBundle:
|
||||
"""Covers PromptFilesBundle construction and path-based categorization."""
|
||||
|
||||
class TestFromRoot:
|
||||
"""Covers bundle creation from resource roots."""
|
||||
|
||||
def test_builds_bundles(self, tmp_path: Path) -> None:
|
||||
"""Ensures from_root builds bundles from discovered markdown docs."""
|
||||
alpha = tmp_path / "prompts" / "alpha"
|
||||
beta = tmp_path / "prompts" / "beta"
|
||||
alpha.mkdir(parents=True)
|
||||
beta.mkdir(parents=True)
|
||||
(alpha / "PROMPT.md").write_text("# alpha\n", encoding="utf-8")
|
||||
(alpha / "notes.md").write_text("notes\n", encoding="utf-8")
|
||||
(beta / "PROMPT.md").write_text("# beta\n", encoding="utf-8")
|
||||
|
||||
bundles = PromptFilesBundle.from_root(tmp_path)
|
||||
|
||||
assert {bundle.slug for bundle in bundles} == {"alpha", "beta"}
|
||||
|
||||
def test_delegates_to_from_docs(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Ensures from_root delegates bundle assembly to from_docs."""
|
||||
alpha = tmp_path / "prompts" / "alpha"
|
||||
beta = tmp_path / "prompts" / "beta"
|
||||
alpha.mkdir(parents=True)
|
||||
beta.mkdir(parents=True)
|
||||
(alpha / "PROMPT.md").write_text("# alpha\n", encoding="utf-8")
|
||||
(alpha / "notes.md").write_text("notes\n", encoding="utf-8")
|
||||
(beta / "PROMPT.md").write_text("# beta\n", encoding="utf-8")
|
||||
|
||||
from_root = PromptFilesBundle.from_root(tmp_path)
|
||||
from_docs = PromptFilesBundle.from_docs(MarkdownDocument.from_root(tmp_path).values())
|
||||
|
||||
assert tuple(from_root) == from_docs
|
||||
|
||||
class TestFromDocs:
|
||||
"""Covers bundle creation from preloaded markdown documents."""
|
||||
|
||||
def test_groups_by_slug(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures from_docs groups documents by prompt slug."""
|
||||
docs = [
|
||||
make_doc("prompts/alpha/PROMPT.md"),
|
||||
make_doc("prompts/alpha/notes.md"),
|
||||
make_doc("prompts/beta/PROMPT.md"),
|
||||
]
|
||||
|
||||
bundles = PromptFilesBundle.from_docs(docs)
|
||||
|
||||
assert {bundle.slug for bundle in bundles} == {"alpha", "beta"}
|
||||
|
||||
def test_one_bundle_per_slug(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures from_docs produces one PromptFilesBundle per slug."""
|
||||
docs = [
|
||||
make_doc("prompts/alpha/PROMPT.md"),
|
||||
make_doc("prompts/alpha/notes.md"),
|
||||
make_doc("prompts/alpha/changelog.md"),
|
||||
]
|
||||
|
||||
bundles = PromptFilesBundle.from_docs(docs)
|
||||
|
||||
assert len(bundles) == 1
|
||||
assert bundles[0].slug == "alpha"
|
||||
|
||||
class TestFromPaths:
|
||||
"""Covers classification of prompt and other documents."""
|
||||
|
||||
def test_selects_prompt_md(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures from_paths selects PROMPT.md as the primary document."""
|
||||
docs = {
|
||||
make_doc("prompts/alpha/PROMPT.md"),
|
||||
make_doc("prompts/alpha/notes.md"),
|
||||
}
|
||||
|
||||
bundle = PromptFilesBundle.from_paths("alpha", docs)
|
||||
|
||||
assert bundle.prompt.relpath.name == "PROMPT.md"
|
||||
|
||||
def test_collects_other_docs(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures from_paths classifies non-prompt docs as other docs."""
|
||||
docs = {
|
||||
make_doc("prompts/alpha/PROMPT.md"),
|
||||
make_doc("prompts/alpha/notes.md"),
|
||||
make_doc("prompts/alpha/changelog.md"),
|
||||
}
|
||||
|
||||
bundle = PromptFilesBundle.from_paths("alpha", docs)
|
||||
|
||||
assert {doc.relpath.as_posix() for doc in bundle.other} == {
|
||||
"prompts/alpha/changelog.md",
|
||||
"prompts/alpha/notes.md",
|
||||
}
|
||||
|
||||
|
||||
class TestGroupPromptPaths:
|
||||
"""Covers grouping markdown documents by derived prompt slug."""
|
||||
|
||||
def test_groups_slugged_docs(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures group_prompt_paths groups only documents with a slug."""
|
||||
docs = [
|
||||
make_doc("prompts/alpha/PROMPT.md"),
|
||||
make_doc("prompts/alpha/notes.md"),
|
||||
make_doc("prompts/beta/PROMPT.md"),
|
||||
]
|
||||
|
||||
grouped = group_prompt_paths(docs)
|
||||
|
||||
assert set(grouped) == {"alpha", "beta"}
|
||||
|
||||
def test_excludes_unslugged_docs(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures group_prompt_paths excludes documents without prompt slugs."""
|
||||
docs = [
|
||||
make_doc("docs/index.md"),
|
||||
make_doc("prompts/legacy.md"),
|
||||
]
|
||||
|
||||
assert group_prompt_paths(docs) == {}
|
||||
|
||||
def test_returns_sets(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures group_prompt_paths returns sets of docs per slug."""
|
||||
grouped = group_prompt_paths([make_doc("prompts/alpha/PROMPT.md")])
|
||||
|
||||
assert isinstance(grouped["alpha"], set)
|
||||
|
||||
def test_stable_grouping(self, make_doc: MakeDoc) -> None:
|
||||
"""Ensures group_prompt_paths behaves consistently after internal sorting."""
|
||||
docs = [
|
||||
make_doc("prompts/beta/PROMPT.md"),
|
||||
make_doc("prompts/alpha/notes.md"),
|
||||
make_doc("prompts/alpha/PROMPT.md"),
|
||||
]
|
||||
|
||||
grouped_forward = group_prompt_paths(docs)
|
||||
grouped_reverse = group_prompt_paths(list(reversed(docs)))
|
||||
|
||||
assert grouped_forward == grouped_reverse
|
||||
@@ -1,78 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from personal_mcp.registry.ingest.document import MarkdownDocument
|
||||
from personal_mcp.registry.models.common import parse_docs_path
|
||||
from personal_mcp.registry.models.registry import DocsRegistry
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def make_markdown_document(
|
||||
relpath: str,
|
||||
*,
|
||||
content: str = "# body\n",
|
||||
frontmatter: str | None = None,
|
||||
) -> MarkdownDocument:
|
||||
"""Builds a markdown ingest document with deterministic defaults."""
|
||||
return MarkdownDocument(
|
||||
relpath=PurePosixPath(relpath),
|
||||
content=content,
|
||||
frontmatter=frontmatter,
|
||||
)
|
||||
|
||||
|
||||
def make_prompt_frontmatter_payload(
|
||||
*,
|
||||
prompt_id: str,
|
||||
name: str | None = None,
|
||||
version: str = "1.0.0",
|
||||
description: str = "demo prompt",
|
||||
tags: tuple[str, ...] = ("testing",),
|
||||
capabilities: tuple[str, ...] | None = None,
|
||||
arguments: dict[str, dict[str, Any]] | None = None,
|
||||
) -> str:
|
||||
"""Builds frontmatter payload YAML for prompt conversion tests."""
|
||||
canonical_name = name or prompt_id
|
||||
payload: dict[str, Any] = {
|
||||
"name": canonical_name,
|
||||
"description": description,
|
||||
"x-personal-mcp": {
|
||||
"id": prompt_id,
|
||||
"version": version,
|
||||
"tags": list(tags),
|
||||
"capabilities": list(capabilities or (f"resource://prompts/{canonical_name}/document",)),
|
||||
"arguments": arguments or {},
|
||||
},
|
||||
}
|
||||
return yaml.safe_dump(payload, sort_keys=False)
|
||||
|
||||
|
||||
def as_markdown(frontmatter_yaml: str, *, body: str = "# body\n") -> str:
|
||||
"""Wraps frontmatter YAML in markdown fence delimiters."""
|
||||
return f"---\n{frontmatter_yaml.strip()}\n---\n{body}"
|
||||
|
||||
|
||||
def assert_model_is_frozen(instance: Any, *, attr: str, value: Any) -> None:
|
||||
"""Asserts pydantic frozen model semantics."""
|
||||
with pytest.raises(ValidationError, match="Instance is frozen"):
|
||||
setattr(instance, attr, value)
|
||||
|
||||
|
||||
class TestGate5ContractValidation:
|
||||
"""Gate 5: canonical resource-path contracts."""
|
||||
class TestDocsPathValidation:
|
||||
"""Covers canonical resource-path contracts."""
|
||||
|
||||
def test_parse_docs_path_returns_pure_posix_path(self) -> None:
|
||||
"""Ensures boundary strings become path objects before publication."""
|
||||
path = parse_docs_path("skills/demo/SKILL.md")
|
||||
path = parse_docs_path("guides/demo.md")
|
||||
|
||||
assert path == PurePosixPath("skills/demo/SKILL.md")
|
||||
assert path == PurePosixPath("guides/demo.md")
|
||||
assert isinstance(path, PurePosixPath)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -80,51 +23,11 @@ class TestGate5ContractValidation:
|
||||
(
|
||||
"/absolute.md",
|
||||
"../outside.md",
|
||||
"skills\\demo\\SKILL.md",
|
||||
"skills//demo/SKILL.md",
|
||||
"skills/demo/README.txt",
|
||||
"guides\\demo.md",
|
||||
"guides//demo.md",
|
||||
"guides/demo.txt",
|
||||
),
|
||||
)
|
||||
def test_parse_docs_path_rejects_invalid_paths(self, value: str) -> None:
|
||||
"""Ensures non-canonical docs paths fail contract validation."""
|
||||
with pytest.raises(ValueError):
|
||||
parse_docs_path(value)
|
||||
|
||||
|
||||
class TestGate6FreezeValidation:
|
||||
"""Gate 6: immutable in-memory registry snapshot semantics."""
|
||||
|
||||
def test_docs_registry_copies_mapping_inputs(self) -> None:
|
||||
"""Ensures registry snapshots are isolated from caller-owned mapping mutations."""
|
||||
index_path = PurePosixPath("index.md")
|
||||
source_docs = {index_path: "# index\n"}
|
||||
registry = DocsRegistry(
|
||||
docs_markdown_by_path=source_docs,
|
||||
docs_markdown_path_index=(index_path,),
|
||||
prompts_by_id={},
|
||||
prompts_in_load_order=(),
|
||||
prompts_summary_in_load_order=(),
|
||||
tag_to_prompt_ids={},
|
||||
)
|
||||
|
||||
source_docs[PurePosixPath("other.md")] = "# other\n"
|
||||
|
||||
assert PurePosixPath("other.md") not in registry.docs_markdown_by_path
|
||||
assert registry.docs_markdown_path_index == (index_path,)
|
||||
|
||||
def test_docs_registry_instance_is_frozen(self) -> None:
|
||||
"""Ensures frozen model prevents attribute reassignment."""
|
||||
registry = DocsRegistry(
|
||||
docs_markdown_by_path={},
|
||||
docs_markdown_path_index=(),
|
||||
prompts_by_id={},
|
||||
prompts_in_load_order=(),
|
||||
prompts_summary_in_load_order=(),
|
||||
tag_to_prompt_ids={},
|
||||
)
|
||||
|
||||
assert_model_is_frozen(
|
||||
registry,
|
||||
attr="docs_markdown_path_index",
|
||||
value=(PurePosixPath("index.md"),),
|
||||
)
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from personal_mcp.registry.ingest.prompt import PromptFilesBundle
|
||||
from personal_mcp.registry.load import _build_prompt_record
|
||||
from personal_mcp.registry.models.registry import PromptSummaryRecord
|
||||
from tests.registry.models.test_document_validation import assert_model_is_frozen
|
||||
from tests.registry.models.test_document_validation import make_markdown_document
|
||||
from tests.registry.models.test_document_validation import make_prompt_frontmatter_payload
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_prompt_bundle(
|
||||
*,
|
||||
slug: str,
|
||||
frontmatter: str | None,
|
||||
other_files: tuple[str, ...] = (),
|
||||
) -> PromptFilesBundle:
|
||||
prompt = make_markdown_document(
|
||||
f"prompts/{slug}/PROMPT.md",
|
||||
frontmatter=frontmatter,
|
||||
)
|
||||
other = tuple(make_markdown_document(f"prompts/{slug}/{filename}") for filename in other_files)
|
||||
return PromptFilesBundle(slug=slug, prompt=prompt, other=other)
|
||||
|
||||
|
||||
class TestPromptValidationGates:
|
||||
"""Gate-oriented validation coverage for prompt conversion."""
|
||||
|
||||
class TestGate1LayoutValidation:
|
||||
"""Gate 1: enforce required source shape before metadata parsing."""
|
||||
|
||||
def test_missing_frontmatter_fails_fast(self) -> None:
|
||||
"""Ensures conversion rejects missing prompt frontmatter at the layout gate."""
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=None)
|
||||
|
||||
with pytest.raises(ValueError, match="missing YAML frontmatter"):
|
||||
_build_prompt_record(bundle=bundle)
|
||||
|
||||
class TestGate2MetadataValidation:
|
||||
"""Gate 2: validate prompt metadata via pydantic models."""
|
||||
|
||||
def test_rejects_non_semver_version(self) -> None:
|
||||
"""Ensures semver violations fail during prompt model validation."""
|
||||
frontmatter = make_prompt_frontmatter_payload(prompt_id="initial", version="not-semver")
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
|
||||
with pytest.raises(ValidationError, match="version must be semver"):
|
||||
_build_prompt_record(bundle=bundle)
|
||||
|
||||
def test_rejects_invalid_argument_names(self) -> None:
|
||||
"""Ensures prompt argument keys use Python-identifier naming rules."""
|
||||
frontmatter = make_prompt_frontmatter_payload(
|
||||
prompt_id="initial",
|
||||
arguments={
|
||||
"invalid-name": {
|
||||
"required": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
|
||||
with pytest.raises(ValidationError, match="invalid prompt argument name"):
|
||||
_build_prompt_record(bundle=bundle)
|
||||
|
||||
def test_rejects_missing_primary_capability(self) -> None:
|
||||
"""Ensures canonical prompt document capability is required."""
|
||||
frontmatter = make_prompt_frontmatter_payload(
|
||||
prompt_id="initial",
|
||||
capabilities=("resource://catalog/prompts_index",),
|
||||
)
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
|
||||
with pytest.raises(ValueError, match="capabilities must include"):
|
||||
_build_prompt_record(bundle=bundle)
|
||||
|
||||
class TestGate3ResourceValidation:
|
||||
"""Gate 3: resolve prompt document resources and argument schema."""
|
||||
|
||||
def test_assigns_canonical_document_uri(self) -> None:
|
||||
"""Ensures prompt records emit canonical document URIs."""
|
||||
frontmatter = make_prompt_frontmatter_payload(prompt_id="initial")
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
|
||||
record = _build_prompt_record(bundle=bundle)
|
||||
|
||||
assert record.document_uri == "resource://prompts/initial/document"
|
||||
assert record.document_relpath == PurePosixPath("prompts/initial/PROMPT.md")
|
||||
|
||||
def test_preserves_argument_schema(self) -> None:
|
||||
"""Ensures argument metadata survives conversion unchanged."""
|
||||
frontmatter = make_prompt_frontmatter_payload(
|
||||
prompt_id="initial",
|
||||
arguments={
|
||||
"topic": {
|
||||
"required": True,
|
||||
"description": "topic to discuss",
|
||||
}
|
||||
},
|
||||
)
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
|
||||
record = _build_prompt_record(bundle=bundle)
|
||||
|
||||
assert record.arguments["topic"].required is True
|
||||
assert record.arguments["topic"].description == "topic to discuss"
|
||||
|
||||
class TestGate5ContractValidation:
|
||||
"""Gate 5: validate model_dump contract shape for API surfaces."""
|
||||
|
||||
def test_prompt_record_model_dump_contains_contract_fields(self) -> None:
|
||||
"""Ensures prompt record serialization includes canonical fields."""
|
||||
frontmatter = make_prompt_frontmatter_payload(prompt_id="initial")
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
record = _build_prompt_record(bundle=bundle)
|
||||
|
||||
dumped = record.model_dump()
|
||||
|
||||
assert dumped["prompt_id"] == "initial"
|
||||
assert dumped["document_uri"] == "resource://prompts/initial/document"
|
||||
assert "arguments" in dumped
|
||||
assert "document_content" in dumped
|
||||
|
||||
def test_prompt_summary_projection_stays_stable(self) -> None:
|
||||
"""Ensures prompt summary shape remains deterministic for index responses."""
|
||||
frontmatter = make_prompt_frontmatter_payload(prompt_id="initial")
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
record = _build_prompt_record(bundle=bundle)
|
||||
summary = PromptSummaryRecord.from_record(record)
|
||||
|
||||
assert summary.model_dump() == {
|
||||
"prompt_id": "initial",
|
||||
"name": "initial",
|
||||
"description": "demo prompt",
|
||||
"tags": ("testing",),
|
||||
"capabilities": ("resource://prompts/initial/document",),
|
||||
"document_uri": "resource://prompts/initial/document",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
class TestGate6FreezeValidation:
|
||||
"""Gate 6: ensure immutable runtime records."""
|
||||
|
||||
def test_prompt_record_instance_is_frozen(self) -> None:
|
||||
"""Ensures validated prompt records cannot be mutated after creation."""
|
||||
frontmatter = make_prompt_frontmatter_payload(prompt_id="initial")
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
record = _build_prompt_record(bundle=bundle)
|
||||
|
||||
assert_model_is_frozen(record, attr="name", value="mutated")
|
||||
|
||||
def test_argument_entry_values_are_frozen(self) -> None:
|
||||
"""Ensures nested prompt argument entries are immutable after publication."""
|
||||
frontmatter = make_prompt_frontmatter_payload(
|
||||
prompt_id="initial",
|
||||
arguments={
|
||||
"topic": {
|
||||
"required": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
bundle = _make_prompt_bundle(slug="initial", frontmatter=frontmatter)
|
||||
record = _build_prompt_record(bundle=bundle)
|
||||
|
||||
assert_model_is_frozen(record.arguments["topic"], attr="required", value=False)
|
||||
@@ -1,47 +0,0 @@
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.models.prompt import PromptArgumentEntry
|
||||
from personal_mcp.registry.models.registry import PromptRecord
|
||||
from personal_mcp.registry.models.registry import PromptSummaryPayload
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_prompt_record() -> PromptRecord:
|
||||
return PromptRecord(
|
||||
prompt_id="demo-prompt",
|
||||
name="demo-prompt",
|
||||
description="demo prompt",
|
||||
version="0.9.0",
|
||||
tags=("testing",),
|
||||
capabilities=("resource://prompts/demo-prompt/document",),
|
||||
arguments={
|
||||
"topic": PromptArgumentEntry(
|
||||
title="Topic",
|
||||
required=True,
|
||||
description="topic to discuss",
|
||||
)
|
||||
},
|
||||
document_uri="resource://prompts/demo-prompt/document",
|
||||
document_relpath=PurePosixPath("prompts/demo-prompt/PROMPT.md"),
|
||||
document_content="# demo",
|
||||
)
|
||||
|
||||
|
||||
def test_prompt_summary_payload_from_record_shape() -> None:
|
||||
record = _make_prompt_record()
|
||||
|
||||
payload = PromptSummaryPayload.from_record(record).model_dump()
|
||||
|
||||
assert payload == {
|
||||
"id": "demo-prompt",
|
||||
"name": "demo-prompt",
|
||||
"description": "demo prompt",
|
||||
"tags": ["testing"],
|
||||
"capabilities": ["resource://prompts/demo-prompt/document"],
|
||||
"version": "0.9.0",
|
||||
"document_uri": "resource://prompts/demo-prompt/document",
|
||||
"detail_uri": "resource://catalog/prompts/demo-prompt",
|
||||
}
|
||||
@@ -3,36 +3,16 @@ from pathlib import PurePosixPath
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.models.registry import DocsRegistry
|
||||
from personal_mcp.registry.models.registry import PromptRecord
|
||||
from personal_mcp.registry.models.registry import PromptSummaryRecord
|
||||
from personal_mcp.registry.read import read_docs_markdown_path
|
||||
from personal_mcp.registry.read import read_prompt_document
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_registry() -> DocsRegistry:
|
||||
prompt_path = PurePosixPath("prompts/demo-prompt/PROMPT.md")
|
||||
index_path = PurePosixPath("index.md")
|
||||
prompt = PromptRecord(
|
||||
prompt_id="demo-prompt",
|
||||
name="demo-prompt",
|
||||
description="demo prompt",
|
||||
version="1.0.0",
|
||||
tags=("testing",),
|
||||
capabilities=("resource://prompts/demo-prompt/document",),
|
||||
arguments={},
|
||||
document_uri="resource://prompts/demo-prompt/document",
|
||||
document_relpath=prompt_path,
|
||||
document_content="# prompt",
|
||||
)
|
||||
return DocsRegistry(
|
||||
docs_markdown_by_path={index_path: "# index"},
|
||||
docs_markdown_path_index=(index_path,),
|
||||
prompts_by_id={prompt.prompt_id: prompt},
|
||||
prompts_in_load_order=(prompt.prompt_id,),
|
||||
prompts_summary_in_load_order=(PromptSummaryRecord.from_record(prompt),),
|
||||
tag_to_prompt_ids={"testing": (prompt.prompt_id,)},
|
||||
)
|
||||
|
||||
|
||||
@@ -47,12 +27,11 @@ def test_reads_docs_path_from_string_boundary() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_rejects_skill_docs_path() -> None:
|
||||
with pytest.raises(KeyError, match="unknown docs path"):
|
||||
read_docs_markdown_path(_make_registry(), "skills/demo/SKILL.md")
|
||||
|
||||
|
||||
def test_rejects_non_posix_docs_path() -> None:
|
||||
with pytest.raises(ValueError, match="POSIX separators"):
|
||||
read_docs_markdown_path(_make_registry(), "skills\\demo\\SKILL.md")
|
||||
|
||||
|
||||
def test_serializes_record_paths_in_document_payloads() -> None:
|
||||
registry = _make_registry()
|
||||
|
||||
assert read_prompt_document(registry, "demo-prompt")["source_path"] == ("docs/prompts/demo-prompt/PROMPT.md")
|
||||
read_docs_markdown_path(_make_registry(), "guides\\demo.md")
|
||||
|
||||
@@ -7,6 +7,8 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport
|
||||
from httpx import AsyncClient
|
||||
from httpx2 import ASGITransport as McpASGITransport
|
||||
from httpx2 import AsyncClient as McpAsyncClient
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
@@ -35,15 +37,15 @@ def mcp_session_factory():
|
||||
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
|
||||
async with (
|
||||
app.router.lifespan_context(app),
|
||||
AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
McpAsyncClient(
|
||||
transport=McpASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
timeout=10.0,
|
||||
) as http_client,
|
||||
streamable_http_client(
|
||||
mcp_url,
|
||||
http_client=http_client,
|
||||
) as (read_stream, write_stream, _),
|
||||
) as (read_stream, write_stream),
|
||||
ClientSession(read_stream, write_stream) as session,
|
||||
):
|
||||
if initialize:
|
||||
|
||||
@@ -35,22 +35,12 @@ class TestMcpHttpEndpoints:
|
||||
"""Covers MCP transport endpoint smoke behavior."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_get_stream_without_support(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
mcp_session_factory,
|
||||
) -> None:
|
||||
"""Ensures GET /mcp returns method not allowed for current transport mode."""
|
||||
response = await client.get(
|
||||
"/mcp",
|
||||
headers={"Accept": "text/event-stream"},
|
||||
)
|
||||
|
||||
async def test_exposes_no_tools(self, mcp_session_factory) -> None:
|
||||
"""Ensures the server publishes only native resource and prompt surfaces."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
# Keep the SDK-backed session in use for this route smoke lane.
|
||||
await mcp_session.list_tools()
|
||||
result = await mcp_session.list_tools()
|
||||
|
||||
assert response.status_code == 405
|
||||
assert result.tools == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accepts_initialize_jsonrpc_request(
|
||||
@@ -61,5 +51,5 @@ class TestMcpHttpEndpoints:
|
||||
async with mcp_session_factory(initialize=False) as mcp_session_uninitialized:
|
||||
initialize_result = await mcp_session_uninitialized.initialize()
|
||||
|
||||
assert initialize_result.protocolVersion
|
||||
assert initialize_result.serverInfo.name
|
||||
assert initialize_result.protocol_version
|
||||
assert initialize_result.server_info.name
|
||||
|
||||
@@ -4,6 +4,16 @@ import pytest
|
||||
|
||||
pytestmark = pytest.mark.smoke
|
||||
|
||||
EXPECTED_PROMPTS = {
|
||||
"authoring",
|
||||
"greenfield-architecture",
|
||||
"jsfiddle-page-layout",
|
||||
"mcp-consumer-repo-shim",
|
||||
"nicegui-component-extraction",
|
||||
"pytest-fill-scaffold",
|
||||
"pytest-scaffold",
|
||||
}
|
||||
|
||||
|
||||
class TestMcpPromptSurface:
|
||||
"""Covers smoke-level MCP prompt discovery and retrieval paths."""
|
||||
@@ -17,8 +27,8 @@ class TestMcpPromptSurface:
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_prompts()
|
||||
|
||||
assert result.prompts
|
||||
assert all(prompt.name for prompt in result.prompts)
|
||||
assert {prompt.name for prompt in result.prompts} == EXPECTED_PROMPTS
|
||||
assert all(prompt.description for prompt in result.prompts)
|
||||
|
||||
class TestPromptResolution:
|
||||
"""Covers MCP prompts/get behavior using native request and response objects."""
|
||||
@@ -27,14 +37,15 @@ class TestMcpPromptSurface:
|
||||
async def test_gets_prompt_as_native_object(self, mcp_session_factory) -> None:
|
||||
"""Ensures prompts/get resolves a listed prompt into structured message objects."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
listed_prompts = await mcp_session.list_prompts()
|
||||
prompt = listed_prompts.prompts[0]
|
||||
arguments = {arg.name: "test" for arg in (prompt.arguments or []) if arg.required}
|
||||
|
||||
resolved_prompt = await mcp_session.get_prompt(
|
||||
name=prompt.name,
|
||||
arguments=arguments or None,
|
||||
name="authoring",
|
||||
arguments={
|
||||
"artifact_type": "skill",
|
||||
"artifact_id": "demo-skill",
|
||||
"goal": "Demonstrate native prompt rendering.",
|
||||
},
|
||||
)
|
||||
|
||||
assert resolved_prompt.messages
|
||||
assert all(message.content for message in resolved_prompt.messages)
|
||||
assert "`artifact_id`: demo-skill" in resolved_prompt.messages[0].content.text
|
||||
|
||||
@@ -6,41 +6,10 @@ import pytest
|
||||
|
||||
pytestmark = pytest.mark.smoke
|
||||
|
||||
RETIRED_TOOL_NAMES = {
|
||||
"search_patterns",
|
||||
"get_pattern_by_id",
|
||||
"get_skill_document_by_id",
|
||||
}
|
||||
|
||||
|
||||
class TestMcpSkillsSurface:
|
||||
"""Covers native skill resources over the HTTP MCP surface."""
|
||||
|
||||
class TestTools:
|
||||
"""Covers generic resource fallback tools for native skills."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lists_resource_fallback_tools(self, mcp_session_factory) -> None:
|
||||
"""Ensures generic resource tools replace skill-specific catalog tools."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_tools()
|
||||
tool_names = {tool.name for tool in result.tools}
|
||||
|
||||
assert {"list_resources", "read_resource"}.issubset(tool_names)
|
||||
assert RETIRED_TOOL_NAMES.isdisjoint(tool_names)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reads_skill_through_fallback_tool(self, mcp_session_factory) -> None:
|
||||
"""Ensures tool-only clients can read a native skill resource."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.call_tool(
|
||||
"read_resource",
|
||||
{"uri": "skill://mcp-details/SKILL.md"},
|
||||
)
|
||||
|
||||
assert result.isError is False
|
||||
assert result.content
|
||||
|
||||
class TestResources:
|
||||
"""Covers native skill resources, manifests, and file templates."""
|
||||
|
||||
@@ -53,17 +22,15 @@ class TestMcpSkillsSurface:
|
||||
|
||||
assert "skill://mcp-details/SKILL.md" in resource_uris
|
||||
assert "skill://mcp-details/_manifest" in resource_uris
|
||||
assert "resource://catalog/skills_index" not in resource_uris
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lists_resource_templates(self, mcp_session_factory) -> None:
|
||||
"""Ensures supporting files use per-skill wildcard templates."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_resource_templates()
|
||||
template_uris = {template.uriTemplate for template in result.resourceTemplates}
|
||||
template_uris = {template.uri_template for template in result.resource_templates}
|
||||
|
||||
assert "skill://mcp-details/{path*}" in template_uris
|
||||
assert "resource://skills/{skill_id}/document" not in template_uris
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reads_manifest_and_supporting_file(self, mcp_session_factory) -> None:
|
||||
|
||||
Reference in New Issue
Block a user