diff --git a/tests/registry/ingest/conftest.py b/tests/registry/ingest/conftest.py index 6e6b06d..6214261 100644 --- a/tests/registry/ingest/conftest.py +++ b/tests/registry/ingest/conftest.py @@ -1,3 +1,18 @@ 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, str], MarkdownDocument]: + def _make_doc(relpath: str, content: str = "# body\n") -> MarkdownDocument: + return MarkdownDocument(relpath=PurePosixPath(relpath), content=content) + + return _make_doc diff --git a/tests/registry/ingest/test_document.py b/tests/registry/ingest/test_document.py index 16188c5..81085b4 100644 --- a/tests/registry/ingest/test_document.py +++ b/tests/registry/ingest/test_document.py @@ -1,5 +1,17 @@ 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.""" @@ -7,54 +19,139 @@ class TestMarkdownDocument: class TestFromRoot: """Covers loading markdown documents from a resource root.""" - def test_keys_by_relpath(self) -> None: + 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") - def test_loads_markdown_only(self) -> None: + 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") - def test_preserves_relpaths(self) -> None: + 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) -> None: + 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") - def test_sets_frontmatter(self) -> None: + 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") - def test_none_frontmatter(self) -> None: + 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 TestSkillSlugProperty: """Covers skill_slug derivation from document relative paths.""" def test_returns_slug(self) -> None: """Ensures skill_slug returns the slug for valid skills paths.""" + doc = MarkdownDocument(relpath=PurePosixPath("skills/demo/SKILL.md"), content="#") + + assert doc.skill_slug == "demo" def test_none_for_non_skill(self) -> None: """Ensures skill_slug is None for non-skills paths.""" + doc = MarkdownDocument(relpath=PurePosixPath("docs/index.md"), content="#") + + assert doc.skill_slug is None def test_none_for_incomplete_skill(self) -> None: """Ensures skill_slug is None for incomplete skills paths.""" + doc = MarkdownDocument(relpath=PurePosixPath("skills/demo"), content="#") + + assert doc.skill_slug is None class TestWalkResources: """Covers recursive resource walking and markdown filtering behavior.""" - def test_yields_markdown(self) -> None: + 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") - def test_ignores_other_suffixes(self) -> None: + relpaths = [path for path, _ in walk_resources(tmp_path)] + + assert relpaths == [ + PurePosixPath("index.md"), + PurePosixPath("skills/alpha/SKILL.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") - def test_sorted_output(self) -> None: + 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") - def test_applies_prefix(self) -> None: + 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: @@ -62,9 +159,18 @@ class TestFrontmatterParsing: 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] diff --git a/tests/registry/ingest/test_skill.py b/tests/registry/ingest/test_skill.py index 9ddc4f2..a3ee8df 100644 --- a/tests/registry/ingest/test_skill.py +++ b/tests/registry/ingest/test_skill.py @@ -1,5 +1,18 @@ 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.skill import SkillFilesBundle +from personal_mcp.registry.ingest.skill import group_skill_paths + +pytestmark = pytest.mark.unit + +MakeDoc = Callable[[str, str], MarkdownDocument] + class TestSkillFilesBundle: """Covers SkillFilesBundle construction and path-based categorization.""" @@ -7,45 +20,152 @@ class TestSkillFilesBundle: class TestFromRoot: """Covers bundle creation from resource roots.""" - def test_builds_bundles(self) -> None: + def test_builds_bundles(self, tmp_path: Path) -> None: """Ensures from_root builds bundles from discovered markdown docs.""" + alpha = tmp_path / "skills" / "alpha" + beta = tmp_path / "skills" / "beta" + (alpha / "references").mkdir(parents=True) + beta.mkdir(parents=True) + (alpha / "SKILL.md").write_text("# alpha\n", encoding="utf-8") + (alpha / "references" / "one.md").write_text("ref\n", encoding="utf-8") + (beta / "SKILL.md").write_text("# beta\n", encoding="utf-8") - def test_delegates_to_from_docs(self) -> None: + bundles = SkillFilesBundle.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 / "skills" / "alpha" + beta = tmp_path / "skills" / "beta" + (alpha / "references").mkdir(parents=True) + beta.mkdir(parents=True) + (alpha / "SKILL.md").write_text("# alpha\n", encoding="utf-8") + (alpha / "references" / "one.md").write_text("ref\n", encoding="utf-8") + (beta / "SKILL.md").write_text("# beta\n", encoding="utf-8") + + from_root = SkillFilesBundle.from_root(tmp_path) + from_docs = SkillFilesBundle.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) -> None: + def test_groups_by_slug(self, make_doc: MakeDoc) -> None: """Ensures from_docs groups documents by skill slug.""" + docs = [ + make_doc("skills/alpha/SKILL.md"), + make_doc("skills/alpha/references/a.md"), + make_doc("skills/beta/SKILL.md"), + ] - def test_one_bundle_per_slug(self) -> None: + bundles = SkillFilesBundle.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 SkillFilesBundle per slug.""" + docs = [ + make_doc("skills/alpha/SKILL.md"), + make_doc("skills/alpha/references/r1.md"), + make_doc("skills/alpha/references/r2.md"), + ] + + bundles = SkillFilesBundle.from_docs(docs) + + assert len(bundles) == 1 + assert bundles[0].slug == "alpha" class TestFromPaths: """Covers classification of skill, reference, and other documents.""" - def test_selects_skill_md(self) -> None: + def test_selects_skill_md(self, make_doc: MakeDoc) -> None: """Ensures from_paths selects SKILL.md as the primary document.""" + docs = { + make_doc("skills/alpha/SKILL.md"), + make_doc("skills/alpha/notes.md"), + } - def test_collects_references(self) -> None: + bundle = SkillFilesBundle.from_paths("alpha", docs) + + assert bundle.skill.relpath.name == "SKILL.md" + + def test_collects_references(self, make_doc: MakeDoc) -> None: """Ensures from_paths captures reference docs under references/.""" + docs = { + make_doc("skills/alpha/SKILL.md"), + make_doc("skills/alpha/references/r1.md"), + make_doc("skills/alpha/references/r2.md"), + make_doc("skills/alpha/notes.md"), + } - def test_collects_other_docs(self) -> None: + bundle = SkillFilesBundle.from_paths("alpha", docs) + + assert {doc.relpath.as_posix() for doc in bundle.references} == { + "skills/alpha/references/r1.md", + "skills/alpha/references/r2.md", + } + + def test_collects_other_docs(self, make_doc: MakeDoc) -> None: """Ensures from_paths classifies non-reference docs as other docs.""" + docs = { + make_doc("skills/alpha/SKILL.md"), + make_doc("skills/alpha/references/r1.md"), + make_doc("skills/alpha/notes.md"), + make_doc("skills/alpha/changelog.md"), + } + + bundle = SkillFilesBundle.from_paths("alpha", docs) + + assert {doc.relpath.as_posix() for doc in bundle.other} == { + "skills/alpha/changelog.md", + "skills/alpha/notes.md", + } class TestGroupSkillPaths: """Covers grouping markdown documents by derived skill slug.""" - def test_groups_slugged_docs(self) -> None: + def test_groups_slugged_docs(self, make_doc: MakeDoc) -> None: """Ensures group_skill_paths groups only documents with a slug.""" + docs = [ + make_doc("skills/alpha/SKILL.md"), + make_doc("skills/alpha/references/r.md"), + make_doc("skills/beta/SKILL.md"), + ] - def test_excludes_unslugged_docs(self) -> None: + grouped = group_skill_paths(docs) + + assert set(grouped) == {"alpha", "beta"} + + def test_excludes_unslugged_docs(self, make_doc: MakeDoc) -> None: """Ensures group_skill_paths excludes documents without skill slugs.""" + docs = [ + make_doc("docs/index.md"), + make_doc("content/usage.md"), + ] - def test_returns_sets(self) -> None: + assert group_skill_paths(docs) == {} + + def test_returns_sets(self, make_doc: MakeDoc) -> None: """Ensures group_skill_paths returns sets of docs per slug.""" + grouped = group_skill_paths([make_doc("skills/alpha/SKILL.md")]) - def test_stable_grouping(self) -> None: + assert isinstance(grouped["alpha"], set) + + def test_stable_grouping(self, make_doc: MakeDoc) -> None: """Ensures group_skill_paths behaves consistently after internal sorting.""" + docs = [ + make_doc("skills/beta/SKILL.md"), + make_doc("skills/alpha/references/a.md"), + make_doc("skills/alpha/SKILL.md"), + ] + + grouped_forward = group_skill_paths(docs) + grouped_reverse = group_skill_paths(list(reversed(docs))) + + assert grouped_forward == grouped_reverse