from __future__ import annotations import json from pathlib import Path import pytest from beaver.plugins.manifest import load_plugin_manifest def _write_manifest(root: Path, payload: dict) -> Path: path = root / "beaver.plugin.json" path.write_text(json.dumps(payload), encoding="utf-8") return path def test_load_plugin_manifest_accepts_declared_skill(tmp_path: Path) -> None: root = tmp_path / "comic" (root / "skills" / "comic").mkdir(parents=True) (root / "skills" / "comic" / "SKILL.md").write_text("# Comic\n", encoding="utf-8") _write_manifest( root, { "schema_version": 1, "id": "baoyu-comic", "name": "Baoyu Comic", "version": "1.2.0", "skills": [{"name": "baoyu-comic", "path": "skills/comic"}], }, ) manifest = load_plugin_manifest(root / "beaver.plugin.json") assert manifest.plugin_id == "baoyu-comic" assert manifest.name == "Baoyu Comic" assert manifest.version == "1.2.0" assert manifest.display_path == "comic/beaver.plugin.json" assert manifest.skills[0].name == "baoyu-comic" assert manifest.skills[0].relative_path == "skills/comic" assert manifest.skills[0].root == root / "skills" / "comic" @pytest.mark.parametrize("value", ["../outside", "/absolute", "skills/../../outside"]) def test_load_plugin_manifest_rejects_escaping_skill_path(tmp_path: Path, value: str) -> None: root = tmp_path / "unsafe" root.mkdir() path = _write_manifest( root, { "schema_version": 1, "id": "unsafe", "name": "Unsafe", "version": "1.0.0", "skills": [{"name": "unsafe", "path": value}], }, ) with pytest.raises(ValueError, match="contained"): load_plugin_manifest(path) @pytest.mark.parametrize("identifier", ["BadName", "-bad", "bad.name", ""]) def test_load_plugin_manifest_rejects_invalid_identifiers(tmp_path: Path, identifier: str) -> None: root = tmp_path / "bad" (root / "skills" / "skill").mkdir(parents=True) (root / "skills" / "skill" / "SKILL.md").write_text("# Skill\n", encoding="utf-8") path = _write_manifest( root, { "schema_version": 1, "id": identifier, "name": "Bad", "version": "1.0.0", "skills": [{"name": "good-skill", "path": "skills/skill"}], }, ) with pytest.raises(ValueError, match="identifier"): load_plugin_manifest(path) def test_load_plugin_manifest_rejects_duplicate_skill_names(tmp_path: Path) -> None: root = tmp_path / "dupe" for dirname in ("one", "two"): (root / "skills" / dirname).mkdir(parents=True) (root / "skills" / dirname / "SKILL.md").write_text("# Skill\n", encoding="utf-8") path = _write_manifest( root, { "schema_version": 1, "id": "dupe", "name": "Duplicate", "version": "1.0.0", "skills": [ {"name": "same", "path": "skills/one"}, {"name": "same", "path": "skills/two"}, ], }, ) with pytest.raises(ValueError, match="duplicate"): load_plugin_manifest(path) def test_load_plugin_manifest_rejects_unsupported_schema_version(tmp_path: Path) -> None: root = tmp_path / "future" root.mkdir() path = _write_manifest( root, { "schema_version": 2, "id": "future", "name": "Future", "version": "2.0.0", "skills": [], }, ) with pytest.raises(ValueError, match="schema"): load_plugin_manifest(path) def test_load_plugin_manifest_requires_skill_md(tmp_path: Path) -> None: root = tmp_path / "missing" (root / "skills" / "missing").mkdir(parents=True) path = _write_manifest( root, { "schema_version": 1, "id": "missing", "name": "Missing", "version": "1.0.0", "skills": [{"name": "missing", "path": "skills/missing"}], }, ) with pytest.raises(ValueError, match="SKILL.md"): load_plugin_manifest(path) def test_load_plugin_manifest_rejects_symlinked_skill_root(tmp_path: Path) -> None: root = tmp_path / "linked" real = root / "real" real.mkdir(parents=True) (real / "SKILL.md").write_text("# Linked\n", encoding="utf-8") (root / "skills").mkdir() (root / "skills" / "linked").symlink_to(real, target_is_directory=True) path = _write_manifest( root, { "schema_version": 1, "id": "linked", "name": "Linked", "version": "1.0.0", "skills": [{"name": "linked", "path": "skills/linked"}], }, ) with pytest.raises(ValueError, match="symlink"): load_plugin_manifest(path)