started model tests
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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.load import _parse_frontmatter
|
||||
from personal_mcp.registry.models.common import _normalize_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_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,
|
||||
depends_on: tuple[str, ...] = (),
|
||||
references: dict[str, dict[str, Any]] | None = None,
|
||||
) -> str:
|
||||
"""Builds frontmatter payload YAML for skill conversion tests."""
|
||||
canonical_name = name or skill_id
|
||||
payload: dict[str, Any] = {
|
||||
"name": canonical_name,
|
||||
"description": description,
|
||||
"x-personal-mcp": {
|
||||
"id": skill_id,
|
||||
"version": version,
|
||||
"tags": list(tags),
|
||||
"capabilities": list(capabilities or (f"resource://skills/{canonical_name}/document",)),
|
||||
"depends_on": list(depends_on),
|
||||
},
|
||||
}
|
||||
if references is not None:
|
||||
payload["x-personal-mcp"]["references"] = references
|
||||
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 TestGate1LayoutValidation:
|
||||
"""Gate 1: source layout and parse shape constraints."""
|
||||
|
||||
def test_parse_frontmatter_requires_payload(self) -> None:
|
||||
"""Ensures missing frontmatter fails before metadata validation."""
|
||||
with pytest.raises(ValueError, match="missing YAML frontmatter"):
|
||||
_parse_frontmatter(None, path=PurePosixPath("skills/alpha/SKILL.md"))
|
||||
|
||||
def test_parse_frontmatter_requires_mapping(self) -> None:
|
||||
"""Ensures non-object YAML payloads are rejected at parse time."""
|
||||
with pytest.raises(TypeError, match="frontmatter must parse to an object"):
|
||||
_parse_frontmatter("- one\n- two\n", path=PurePosixPath("skills/alpha/SKILL.md"))
|
||||
|
||||
def test_parse_frontmatter_accepts_mapping(self) -> None:
|
||||
"""Ensures valid mapping payload is returned for downstream validation."""
|
||||
parsed = _parse_frontmatter("name: alpha\n", path=PurePosixPath("skills/alpha/SKILL.md"))
|
||||
|
||||
assert parsed == {"name": "alpha"}
|
||||
|
||||
|
||||
class TestGate5ContractValidation:
|
||||
"""Gate 5: canonical resource-path contract normalization."""
|
||||
|
||||
def test_normalize_docs_path_keeps_posix_relative_paths(self) -> None:
|
||||
"""Ensures docs paths remain normalized before registry publication."""
|
||||
assert _normalize_docs_path("skills/demo/SKILL.md") == "skills/demo/SKILL.md"
|
||||
|
||||
def test_normalize_docs_path_rejects_parent_traversal(self) -> None:
|
||||
"""Ensures traversal attempts fail contract validation."""
|
||||
with pytest.raises(ValueError, match="normalized docs-relative path"):
|
||||
_normalize_docs_path("../outside.md")
|
||||
|
||||
|
||||
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."""
|
||||
source_docs = {"index.md": "# 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.md",),
|
||||
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["other.md"] = "# other\n"
|
||||
|
||||
assert "other.md" not in registry.docs_markdown_by_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=("index.md",))
|
||||
Reference in New Issue
Block a user