Files
prompts/tests/registry/models/test_document_validation.py
T

185 lines
6.3 KiB
Python

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 ReferenceEntry
from personal_mcp.registry.models.common import parse_docs_path
from personal_mcp.registry.models.common import parse_reference_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_skill_frontmatter_payload(
*,
skill_id: str,
name: str | None = None,
version: str = "1.0.0",
description: str = "demo skill",
tags: tuple[str, ...] = ("testing",),
capabilities: tuple[str, ...] | None = None,
references: dict[str, dict[str, Any]] | None = None,
) -> str:
"""Builds frontmatter payload YAML for skill conversion tests."""
canonical_name = name or skill_id
x_personal_mcp: dict[str, Any] = {
"id": skill_id,
"version": version,
"tags": list(tags),
"capabilities": list(capabilities or (f"resource://skills/{canonical_name}/document",)),
}
if references is not None:
x_personal_mcp["references"] = references
payload: dict[str, Any] = {
"name": canonical_name,
"description": description,
"x-personal-mcp": x_personal_mcp,
}
return yaml.safe_dump(payload, sort_keys=False)
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."""
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")
assert path == PurePosixPath("skills/demo/SKILL.md")
assert isinstance(path, PurePosixPath)
@pytest.mark.parametrize(
"value",
(
"/absolute.md",
"../outside.md",
"skills\\demo\\SKILL.md",
"skills//demo/SKILL.md",
"skills/demo/README.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)
def test_reference_entry_materializes_reference_path(self) -> None:
"""Ensures authored reference strings become constrained path objects."""
entry = ReferenceEntry.model_validate({"path": "references/guides/setup.md"})
assert entry.path == PurePosixPath("references/guides/setup.md")
assert parse_reference_path(entry.path) == entry.path
@pytest.mark.parametrize("value", ("guide.md", "other/guide.md", "references.md"))
def test_reference_path_stays_under_references(self, value: str) -> None:
"""Ensures in-skill references remain below the references directory."""
with pytest.raises(ValueError, match="stay under references"):
parse_reference_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(
skills_by_id={},
skills_in_load_order=(),
skills_summary_in_load_order=(),
docs_markdown_by_path=source_docs,
docs_markdown_path_index=(index_path,),
tag_to_skill_ids={},
capability_to_skill_ids={},
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(
skills_by_id={},
skills_in_load_order=(),
skills_summary_in_load_order=(),
docs_markdown_by_path={},
docs_markdown_path_index=(),
tag_to_skill_ids={},
capability_to_skill_ids={},
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"),),
)