started model tests

This commit is contained in:
John Lancaster
2026-06-21 17:09:03 -05:00
parent 37fa9b6c6f
commit c189677717
7 changed files with 582 additions and 11 deletions
@@ -0,0 +1,205 @@
from __future__ import annotations
from pathlib import PurePosixPath
import pytest
from pydantic import ValidationError
from personal_mcp.registry.ingest.document import MarkdownDocument
from personal_mcp.registry.ingest.skill import SkillFilesBundle
from personal_mcp.registry.load import _build_skill_record
from personal_mcp.registry.load import _to_summary
from personal_mcp.registry.load import load_docs_registry
from tests.registry.models.test_document_validation import as_markdown
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_skill_frontmatter_payload
pytestmark = pytest.mark.unit
def _make_skill_bundle(
*,
slug: str,
frontmatter: str | None,
reference_files: tuple[str, ...] = (),
other_files: tuple[str, ...] = (),
) -> SkillFilesBundle:
skill = make_markdown_document(
f"skills/{slug}/SKILL.md",
frontmatter=frontmatter,
)
references = tuple(make_markdown_document(f"skills/{slug}/references/{filename}") for filename in reference_files)
other = tuple(make_markdown_document(f"skills/{slug}/{filename}") for filename in other_files)
return SkillFilesBundle(slug=slug, skill=skill, references=references, other=other)
def _docs_index(bundle: SkillFilesBundle) -> dict[PurePosixPath, MarkdownDocument]:
docs = {bundle.skill.relpath: bundle.skill}
for doc in bundle.references:
docs[doc.relpath] = doc
for doc in bundle.other:
docs[doc.relpath] = doc
return docs
class TestSkillValidationGates:
"""Gate-oriented validation coverage for skill conversion."""
class TestGate1LayoutValidation:
"""Gate 1: enforce required source shape before metadata parsing."""
def test_missing_frontmatter_fails_fast(self) -> None:
"""Ensures conversion rejects missing frontmatter at the layout gate."""
bundle = _make_skill_bundle(slug="alpha", frontmatter=None)
with pytest.raises(ValueError, match="missing YAML frontmatter"):
_build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
class TestGate2MetadataValidation:
"""Gate 2: validate skill metadata via pydantic models."""
def test_rejects_non_semver_version(self) -> None:
"""Ensures semver violations fail during frontmatter model validation."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha", version="not-semver")
bundle = _make_skill_bundle(slug="alpha", frontmatter=frontmatter)
with pytest.raises(ValidationError, match="version must be semver"):
_build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
def test_rejects_reserved_name_tokens(self) -> None:
"""Ensures reserved words remain blocked for skill names."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha", name="claude-skill")
bundle = _make_skill_bundle(slug="alpha", frontmatter=frontmatter)
with pytest.raises(ValidationError, match="reserved words anthropic or claude"):
_build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
def test_rejects_missing_primary_capability(self) -> None:
"""Ensures canonical document capability is required."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha", capabilities=("resource://docs/index.md",))
bundle = _make_skill_bundle(slug="alpha", frontmatter=frontmatter)
with pytest.raises(ValueError, match="capabilities must include"):
_build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
class TestGate3ResourceValidation:
"""Gate 3: resolve document and reference resources deterministically."""
def test_discovers_reference_from_filename(self) -> None:
"""Ensures top-level reference files are converted into reference records."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha")
bundle = _make_skill_bundle(
slug="alpha",
frontmatter=frontmatter,
reference_files=("quick-start.md",),
)
record = _build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
assert set(record.references) == {"quick-start"}
assert record.references["quick-start"].uri == "resource://skills/alpha/references/quick-start"
assert record.references["quick-start"].title == "Quick Start"
def test_declared_reference_requires_existing_document(self) -> None:
"""Ensures declared reference paths must map to discovered markdown files."""
frontmatter = make_skill_frontmatter_payload(
skill_id="alpha",
references={
"guide": {
"path": "references/guide.md",
"title": "Guide",
}
},
)
bundle = _make_skill_bundle(slug="alpha", frontmatter=frontmatter)
with pytest.raises(KeyError, match="reference document not found"):
_build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
class TestGate4GraphValidation:
"""Gate 4: validate dependency graph coherence across skills."""
def test_unknown_dependency_fails_registry_load(self, tmp_path) -> None:
"""Ensures unresolved depends_on targets abort registry publication."""
skill_dir = tmp_path / "skills" / "alpha"
skill_dir.mkdir(parents=True)
frontmatter = make_skill_frontmatter_payload(skill_id="alpha", depends_on=("beta",))
(skill_dir / "SKILL.md").write_text(as_markdown(frontmatter), encoding="utf-8")
with pytest.raises(ValueError, match="depends_on unknown skill"):
load_docs_registry(package_anchor="personal_mcp", docs_root=str(tmp_path))
def test_resolved_dependency_succeeds_registry_load(self, tmp_path) -> None:
"""Ensures valid dependency graph survives graph validation."""
alpha_dir = tmp_path / "skills" / "alpha"
beta_dir = tmp_path / "skills" / "beta"
alpha_dir.mkdir(parents=True)
beta_dir.mkdir(parents=True)
alpha_frontmatter = make_skill_frontmatter_payload(skill_id="alpha", depends_on=("beta",))
beta_frontmatter = make_skill_frontmatter_payload(skill_id="beta")
(alpha_dir / "SKILL.md").write_text(as_markdown(alpha_frontmatter), encoding="utf-8")
(beta_dir / "SKILL.md").write_text(as_markdown(beta_frontmatter), encoding="utf-8")
registry = load_docs_registry(package_anchor="personal_mcp", docs_root=str(tmp_path))
assert registry.skills_by_id["alpha"].depends_on == ("beta",)
class TestGate5ContractValidation:
"""Gate 5: validate model_dump contract shape for API surfaces."""
def test_skill_record_model_dump_contains_contract_fields(self) -> None:
"""Ensures skill record serialization contains expected resource contract keys."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha")
bundle = _make_skill_bundle(slug="alpha", frontmatter=frontmatter)
record = _build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
dumped = record.model_dump()
assert dumped["skill_id"] == "alpha"
assert dumped["document_uri"] == "resource://skills/alpha/document"
assert "references" in dumped
assert "document_content" in dumped
def test_skill_summary_projection_stays_stable(self) -> None:
"""Ensures summary projection keeps only index-safe fields."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha")
bundle = _make_skill_bundle(slug="alpha", frontmatter=frontmatter)
record = _build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
summary = _to_summary(record)
assert summary.model_dump() == {
"skill_id": "alpha",
"name": "alpha",
"description": "demo skill",
"tags": ("testing",),
"capabilities": ("resource://skills/alpha/document",),
"document_uri": "resource://skills/alpha/document",
"version": "1.0.0",
}
class TestGate6FreezeValidation:
"""Gate 6: ensure immutable runtime records."""
def test_skill_record_instance_is_frozen(self) -> None:
"""Ensures validated skill records cannot be mutated after creation."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha")
bundle = _make_skill_bundle(slug="alpha", frontmatter=frontmatter)
record = _build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
assert_model_is_frozen(record, attr="name", value="mutated")
def test_reference_record_values_are_frozen(self) -> None:
"""Ensures nested reference records are immutable after publication."""
frontmatter = make_skill_frontmatter_payload(skill_id="alpha")
bundle = _make_skill_bundle(
slug="alpha",
frontmatter=frontmatter,
reference_files=("guide.md",),
)
record = _build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
with pytest.raises(ValidationError, match="Instance is frozen"):
record.references["guide"].title = "Mutated"