WIP simplifying load/startup
This commit is contained in:
@@ -8,8 +8,9 @@ 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.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
|
||||
@@ -95,37 +96,43 @@ def assert_model_is_frozen(instance: Any, *, attr: str, value: Any) -> None:
|
||||
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."""
|
||||
"""Gate 5: canonical resource-path contracts."""
|
||||
|
||||
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_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")
|
||||
|
||||
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")
|
||||
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:
|
||||
@@ -133,13 +140,14 @@ class TestGate6FreezeValidation:
|
||||
|
||||
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"}
|
||||
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.md",),
|
||||
docs_markdown_path_index=(index_path,),
|
||||
tag_to_skill_ids={},
|
||||
capability_to_skill_ids={},
|
||||
prompts_by_id={},
|
||||
@@ -148,9 +156,10 @@ class TestGate6FreezeValidation:
|
||||
tag_to_prompt_ids={},
|
||||
)
|
||||
|
||||
source_docs["other.md"] = "# other\n"
|
||||
source_docs[PurePosixPath("other.md")] = "# other\n"
|
||||
|
||||
assert "other.md" not in registry.docs_markdown_by_path
|
||||
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."""
|
||||
@@ -168,4 +177,8 @@ class TestGate6FreezeValidation:
|
||||
tag_to_prompt_ids={},
|
||||
)
|
||||
|
||||
assert_model_is_frozen(registry, attr="docs_markdown_path_index", value=("index.md",))
|
||||
assert_model_is_frozen(
|
||||
registry,
|
||||
attr="docs_markdown_path_index",
|
||||
value=(PurePosixPath("index.md"),),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user