161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
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)
|