migration

This commit is contained in:
John Lancaster
2026-08-07 20:07:21 -05:00
parent 2a2700b78c
commit 5b6d5aaec4
70 changed files with 1182 additions and 3633 deletions
+25
View File
@@ -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", {})
+52
View File
@@ -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
-18
View File
@@ -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
+8 -19
View File
@@ -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)
+12 -161
View File
@@ -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"
-154
View File
@@ -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",
}
+6 -27
View File
@@ -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")
+5 -3
View File
@@ -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:
+6 -16
View File
@@ -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
+19 -8
View File
@@ -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
+1 -34
View File
@@ -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: