v1.3.0
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai> Co-Authored-By: Clément Drouin <clement.drouin@mistral.ai> Co-Authored-by: Thiago Padilha <thiago@coplane.com>
This commit is contained in:
parent
2e1e15120d
commit
078693fc64
67 changed files with 3959 additions and 819 deletions
59
tests/skills/conftest.py
Normal file
59
tests/skills/conftest.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def skills_dir(tmp_path: Path) -> Path:
|
||||
"""Create a temporary skills directory."""
|
||||
skills = tmp_path / "skills"
|
||||
skills.mkdir()
|
||||
return skills
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def skill_config(skills_dir: Path) -> VibeConfig:
|
||||
return VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
|
||||
|
||||
def create_skill(
|
||||
skills_dir: Path,
|
||||
name: str,
|
||||
description: str = "A test skill",
|
||||
*,
|
||||
license: str | None = None,
|
||||
compatibility: str | None = None,
|
||||
metadata: dict[str, str] | None = None,
|
||||
allowed_tools: str | None = None,
|
||||
body: str = "## Instructions\n\nTest instructions here.",
|
||||
) -> Path:
|
||||
skill_dir = skills_dir / name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
frontmatter: dict[str, object] = {"name": name, "description": description}
|
||||
if license:
|
||||
frontmatter["license"] = license
|
||||
if compatibility:
|
||||
frontmatter["compatibility"] = compatibility
|
||||
if metadata:
|
||||
frontmatter["metadata"] = metadata
|
||||
if allowed_tools:
|
||||
frontmatter["allowed-tools"] = allowed_tools
|
||||
|
||||
yaml_str = yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True)
|
||||
content = f"---\n{yaml_str}---\n\n{body}"
|
||||
|
||||
skill_file = skill_dir / "SKILL.md"
|
||||
skill_file.write_text(content, encoding="utf-8")
|
||||
|
||||
return skill_dir
|
||||
275
tests/skills/test_manager.py
Normal file
275
tests/skills/test_manager.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.skills.conftest import create_skill
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config() -> VibeConfig:
|
||||
return VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def skill_manager(config: VibeConfig) -> SkillManager:
|
||||
return SkillManager(config)
|
||||
|
||||
|
||||
class TestSkillManagerDiscovery:
|
||||
def test_discovers_no_skills_when_directory_empty(
|
||||
self, skill_manager: SkillManager
|
||||
) -> None:
|
||||
assert skill_manager.available_skills == {}
|
||||
|
||||
def test_discovers_skill_from_skill_paths(self, skills_dir: Path) -> None:
|
||||
create_skill(skills_dir, "test-skill", "A test skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
assert "test-skill" in manager.available_skills
|
||||
assert manager.available_skills["test-skill"].description == "A test skill"
|
||||
|
||||
def test_discovers_multiple_skills(self, skills_dir: Path) -> None:
|
||||
create_skill(skills_dir, "skill-one", "First skill")
|
||||
create_skill(skills_dir, "skill-two", "Second skill")
|
||||
create_skill(skills_dir, "skill-three", "Third skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
assert len(manager.available_skills) == 3
|
||||
assert "skill-one" in manager.available_skills
|
||||
assert "skill-two" in manager.available_skills
|
||||
assert "skill-three" in manager.available_skills
|
||||
|
||||
def test_ignores_directories_without_skill_md(self, skills_dir: Path) -> None:
|
||||
# Create a directory that's not a skill
|
||||
not_a_skill = skills_dir / "not-a-skill"
|
||||
not_a_skill.mkdir()
|
||||
(not_a_skill / "README.md").write_text("Not a skill")
|
||||
|
||||
# Create a valid skill
|
||||
create_skill(skills_dir, "valid-skill", "A valid skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert "valid-skill" in skills
|
||||
assert "not-a-skill" not in skills
|
||||
|
||||
def test_ignores_files_in_skills_directory(self, skills_dir: Path) -> None:
|
||||
# Create a file in the skills directory (not a directory)
|
||||
(skills_dir / "not-a-directory.md").write_text("Just a file")
|
||||
|
||||
# Create a valid skill
|
||||
create_skill(skills_dir, "valid-skill", "A valid skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert "valid-skill" in skills
|
||||
|
||||
|
||||
class TestSkillManagerParsing:
|
||||
def test_parses_all_skill_fields(self, skills_dir: Path) -> None:
|
||||
create_skill(
|
||||
skills_dir,
|
||||
"full-skill",
|
||||
"A skill with all fields",
|
||||
license="MIT",
|
||||
compatibility="Requires git",
|
||||
metadata={"author": "Test Author", "version": "1.0"},
|
||||
allowed_tools="bash read_file",
|
||||
)
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skill = manager.get_skill("full-skill")
|
||||
assert skill is not None
|
||||
assert skill.name == "full-skill"
|
||||
assert skill.description == "A skill with all fields"
|
||||
assert skill.license == "MIT"
|
||||
assert skill.compatibility == "Requires git"
|
||||
assert skill.metadata == {"author": "Test Author", "version": "1.0"}
|
||||
assert skill.allowed_tools == ["bash", "read_file"]
|
||||
|
||||
def test_sets_correct_skill_path(self, skills_dir: Path) -> None:
|
||||
create_skill(skills_dir, "test-skill", "A test skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skill = manager.get_skill("test-skill")
|
||||
assert skill is not None
|
||||
assert skill.skill_path == skills_dir / "test-skill" / "SKILL.md"
|
||||
assert skill.skill_dir == skills_dir / "test-skill"
|
||||
|
||||
def test_skips_skill_with_invalid_frontmatter(self, skills_dir: Path) -> None:
|
||||
# Create an invalid skill
|
||||
invalid_skill_dir = skills_dir / "invalid-skill"
|
||||
invalid_skill_dir.mkdir()
|
||||
(invalid_skill_dir / "SKILL.md").write_text("No frontmatter here")
|
||||
|
||||
# Create a valid skill
|
||||
create_skill(skills_dir, "valid-skill", "A valid skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert "valid-skill" in skills
|
||||
assert "invalid-skill" not in skills
|
||||
|
||||
def test_skips_skill_with_missing_required_fields(self, skills_dir: Path) -> None:
|
||||
# Create skill missing description
|
||||
missing_desc_dir = skills_dir / "missing-desc"
|
||||
missing_desc_dir.mkdir()
|
||||
(missing_desc_dir / "SKILL.md").write_text("---\nname: missing-desc\n---\n")
|
||||
|
||||
# Create a valid skill
|
||||
create_skill(skills_dir, "valid-skill", "A valid skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert "valid-skill" in skills
|
||||
|
||||
|
||||
class TestSkillManagerSearchPaths:
|
||||
def test_discovers_from_multiple_skill_paths(self, tmp_path: Path) -> None:
|
||||
# Create two separate skill directories
|
||||
skills_dir_1 = tmp_path / "skills1"
|
||||
skills_dir_1.mkdir()
|
||||
create_skill(skills_dir_1, "skill-from-dir1", "Skill from directory 1")
|
||||
|
||||
skills_dir_2 = tmp_path / "skills2"
|
||||
skills_dir_2.mkdir()
|
||||
create_skill(skills_dir_2, "skill-from-dir2", "Skill from directory 2")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir_1, skills_dir_2],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 2
|
||||
assert "skill-from-dir1" in skills
|
||||
assert "skill-from-dir2" in skills
|
||||
|
||||
def test_first_discovered_wins_for_duplicates(self, tmp_path: Path) -> None:
|
||||
# Create two directories with the same skill name
|
||||
skills_dir_1 = tmp_path / "skills1"
|
||||
skills_dir_1.mkdir()
|
||||
create_skill(skills_dir_1, "duplicate-skill", "First version")
|
||||
|
||||
skills_dir_2 = tmp_path / "skills2"
|
||||
skills_dir_2.mkdir()
|
||||
create_skill(skills_dir_2, "duplicate-skill", "Second version")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir_1, skills_dir_2],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert skills["duplicate-skill"].description == "First version"
|
||||
|
||||
def test_ignores_nonexistent_skill_paths(self, tmp_path: Path) -> None:
|
||||
skills_dir = tmp_path / "skills"
|
||||
skills_dir.mkdir()
|
||||
create_skill(skills_dir, "valid-skill", "A valid skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir, tmp_path / "nonexistent"],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
assert len(manager.available_skills) == 1
|
||||
assert "valid-skill" in manager.available_skills
|
||||
|
||||
|
||||
class TestSkillManagerGetSkill:
|
||||
def test_returns_skill_by_name(self, skills_dir: Path) -> None:
|
||||
create_skill(skills_dir, "test-skill", "A test skill")
|
||||
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(config)
|
||||
|
||||
skill = manager.get_skill("test-skill")
|
||||
assert skill is not None
|
||||
assert skill.name == "test-skill"
|
||||
|
||||
def test_returns_none_for_unknown_skill(self, skill_manager: SkillManager) -> None:
|
||||
assert skill_manager.get_skill("nonexistent-skill") is None
|
||||
188
tests/skills/test_models.py
Normal file
188
tests/skills/test_models.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
|
||||
from vibe.core.skills.models import SkillInfo, SkillMetadata
|
||||
|
||||
|
||||
class TestSkillMetadata:
|
||||
def test_creates_with_required_fields(self) -> None:
|
||||
meta = SkillMetadata(name="test-skill", description="A test skill")
|
||||
|
||||
assert meta.name == "test-skill"
|
||||
assert meta.description == "A test skill"
|
||||
assert meta.license is None
|
||||
assert meta.compatibility is None
|
||||
assert meta.metadata == {}
|
||||
assert meta.allowed_tools == []
|
||||
|
||||
def test_creates_with_all_fields(self) -> None:
|
||||
meta = SkillMetadata(
|
||||
name="full-skill",
|
||||
description="A skill with all fields",
|
||||
license="MIT",
|
||||
compatibility="Requires git",
|
||||
metadata={"author": "Test Author", "version": "1.0"},
|
||||
allowed_tools=["bash", "read_file"],
|
||||
)
|
||||
|
||||
assert meta.name == "full-skill"
|
||||
assert meta.description == "A skill with all fields"
|
||||
assert meta.license == "MIT"
|
||||
assert meta.compatibility == "Requires git"
|
||||
assert meta.metadata == {"author": "Test Author", "version": "1.0"}
|
||||
assert meta.allowed_tools == ["bash", "read_file"]
|
||||
|
||||
def test_raises_error_for_uppercase_name(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(name="Test-SKILL", description="A test skill")
|
||||
assert "name" in str(exc_info.value).lower()
|
||||
|
||||
def test_raises_error_for_invalid_chars_in_name(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(name="test_skill@v1.0", description="A test skill")
|
||||
assert "name" in str(exc_info.value).lower()
|
||||
|
||||
def test_raises_error_for_consecutive_hyphens(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(name="test--skill", description="A test skill")
|
||||
assert "name" in str(exc_info.value).lower()
|
||||
|
||||
def test_raises_error_for_leading_trailing_hyphens(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(name="-test-skill-", description="A test skill")
|
||||
assert "name" in str(exc_info.value).lower()
|
||||
|
||||
def test_parses_allowed_tools_from_space_delimited_string(self) -> None:
|
||||
meta = SkillMetadata(
|
||||
name="test",
|
||||
description="A test skill",
|
||||
allowed_tools="bash read_file grep", # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert meta.allowed_tools == ["bash", "read_file", "grep"]
|
||||
|
||||
def test_parses_allowed_tools_from_list(self) -> None:
|
||||
meta = SkillMetadata(
|
||||
name="test", description="A test skill", allowed_tools=["bash", "read_file"]
|
||||
)
|
||||
|
||||
assert meta.allowed_tools == ["bash", "read_file"]
|
||||
|
||||
def test_parses_allowed_tools_handles_none(self) -> None:
|
||||
meta = SkillMetadata(
|
||||
name="test",
|
||||
description="A test skill",
|
||||
allowed_tools=None, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert meta.allowed_tools == []
|
||||
|
||||
def test_normalizes_metadata_values_to_strings(self) -> None:
|
||||
meta = SkillMetadata(
|
||||
name="test",
|
||||
description="A test skill",
|
||||
metadata={"version": 1.0, "count": 42}, # type: ignore[dict-item]
|
||||
)
|
||||
|
||||
assert meta.metadata == {"version": "1.0", "count": "42"}
|
||||
|
||||
def test_raises_error_for_missing_name(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(description="A test skill") # type: ignore[call-arg]
|
||||
|
||||
assert "name" in str(exc_info.value)
|
||||
|
||||
def test_raises_error_for_missing_description(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(name="test") # type: ignore[call-arg]
|
||||
|
||||
assert "description" in str(exc_info.value)
|
||||
|
||||
def test_raises_error_for_empty_name(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(name="", description="A test skill")
|
||||
|
||||
assert "name" in str(exc_info.value).lower()
|
||||
|
||||
def test_raises_error_for_empty_description(self) -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SkillMetadata(name="test", description="")
|
||||
|
||||
assert "description" in str(exc_info.value).lower()
|
||||
|
||||
|
||||
class TestSkillInfo:
|
||||
def test_creates_from_metadata(self, tmp_path: Path) -> None:
|
||||
skill_path = tmp_path / "test-skill" / "SKILL.md"
|
||||
skill_path.parent.mkdir()
|
||||
skill_path.touch()
|
||||
|
||||
meta = SkillMetadata(
|
||||
name="test-skill", description="A test skill", license="MIT"
|
||||
)
|
||||
info = SkillInfo.from_metadata(meta, skill_path)
|
||||
|
||||
assert info.name == "test-skill"
|
||||
assert info.description == "A test skill"
|
||||
assert info.license == "MIT"
|
||||
assert info.skill_path == skill_path.resolve()
|
||||
assert info.skill_dir == skill_path.parent.resolve()
|
||||
|
||||
def test_creates_with_all_fields(self, tmp_path: Path) -> None:
|
||||
skill_path = tmp_path / "full-skill" / "SKILL.md"
|
||||
skill_path.parent.mkdir()
|
||||
skill_path.touch()
|
||||
|
||||
info = SkillInfo(
|
||||
name="full-skill",
|
||||
description="A skill with all fields",
|
||||
license="Apache-2.0",
|
||||
compatibility="git, docker",
|
||||
metadata={"author": "Test"},
|
||||
allowed_tools=["bash"],
|
||||
skill_path=skill_path,
|
||||
)
|
||||
|
||||
assert info.name == "full-skill"
|
||||
assert info.description == "A skill with all fields"
|
||||
assert info.license == "Apache-2.0"
|
||||
assert info.compatibility == "git, docker"
|
||||
assert info.metadata == {"author": "Test"}
|
||||
assert info.allowed_tools == ["bash"]
|
||||
assert info.skill_path == skill_path
|
||||
assert info.skill_dir == skill_path.parent.resolve()
|
||||
|
||||
def test_from_metadata_resolves_paths(self, tmp_path: Path) -> None:
|
||||
skill_path = tmp_path / "test-skill" / "SKILL.md"
|
||||
skill_path.parent.mkdir()
|
||||
skill_path.touch()
|
||||
|
||||
meta = SkillMetadata(name="test-skill", description="A test skill")
|
||||
info = SkillInfo.from_metadata(meta, skill_path)
|
||||
|
||||
assert info.skill_path.is_absolute()
|
||||
assert info.skill_dir.is_absolute()
|
||||
|
||||
def test_inherits_all_metadata_fields(self, tmp_path: Path) -> None:
|
||||
skill_path = tmp_path / "test-skill" / "SKILL.md"
|
||||
skill_path.parent.mkdir()
|
||||
skill_path.touch()
|
||||
|
||||
meta = SkillMetadata(
|
||||
name="test-skill",
|
||||
description="A test skill",
|
||||
license="MIT",
|
||||
compatibility="Requires Python 3.12",
|
||||
metadata={"key": "value"},
|
||||
allowed_tools=["bash", "grep"],
|
||||
)
|
||||
info = SkillInfo.from_metadata(meta, skill_path)
|
||||
|
||||
assert info.license == meta.license
|
||||
assert info.compatibility == meta.compatibility
|
||||
assert info.metadata == meta.metadata
|
||||
assert info.allowed_tools == meta.allowed_tools
|
||||
115
tests/skills/test_parser.py
Normal file
115
tests/skills/test_parser.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.skills.parser import SkillParseError, parse_frontmatter
|
||||
|
||||
|
||||
class TestParseFrontmatter:
|
||||
def test_parses_valid_frontmatter(self) -> None:
|
||||
content = """---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
|
||||
## Body content here
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
|
||||
assert frontmatter["name"] == "test-skill"
|
||||
assert frontmatter["description"] == "A test skill"
|
||||
assert "## Body content here" in body
|
||||
|
||||
def test_parses_frontmatter_with_all_fields(self) -> None:
|
||||
content = """---
|
||||
name: full-skill
|
||||
description: A skill with all fields
|
||||
license: MIT
|
||||
compatibility: Requires git
|
||||
metadata:
|
||||
author: Test Author
|
||||
version: "1.0"
|
||||
allowed-tools: bash read_file
|
||||
---
|
||||
|
||||
Instructions here.
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
|
||||
assert frontmatter["name"] == "full-skill"
|
||||
assert frontmatter["description"] == "A skill with all fields"
|
||||
assert frontmatter["license"] == "MIT"
|
||||
assert frontmatter["compatibility"] == "Requires git"
|
||||
assert frontmatter["metadata"]["author"] == "Test Author"
|
||||
assert frontmatter["metadata"]["version"] == "1.0"
|
||||
assert frontmatter["allowed-tools"] == "bash read_file"
|
||||
assert "Instructions here." in body
|
||||
|
||||
def test_raises_error_for_missing_frontmatter(self) -> None:
|
||||
content = "Just markdown content without frontmatter"
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
|
||||
assert "Missing or invalid YAML frontmatter" in str(exc_info.value)
|
||||
|
||||
def test_raises_error_for_unclosed_frontmatter(self) -> None:
|
||||
content = """---
|
||||
name: incomplete
|
||||
description: Missing closing delimiter
|
||||
"""
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
|
||||
assert "Missing or invalid YAML frontmatter" in str(exc_info.value)
|
||||
|
||||
def test_raises_error_for_invalid_yaml(self) -> None:
|
||||
content = """---
|
||||
name: [invalid yaml
|
||||
description: broken
|
||||
---
|
||||
|
||||
Body here.
|
||||
"""
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
|
||||
assert "Invalid YAML frontmatter" in str(exc_info.value)
|
||||
|
||||
def test_raises_error_for_non_dict_frontmatter(self) -> None:
|
||||
content = """---
|
||||
- item1
|
||||
- item2
|
||||
---
|
||||
|
||||
Body here.
|
||||
"""
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
|
||||
assert "must be a mapping" in str(exc_info.value)
|
||||
|
||||
def test_handles_empty_frontmatter(self) -> None:
|
||||
content = """---
|
||||
---
|
||||
|
||||
Body content.
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
|
||||
assert frontmatter == {}
|
||||
assert "Body content." in body
|
||||
|
||||
def test_handles_frontmatter_with_no_body(self) -> None:
|
||||
content = """---
|
||||
name: minimal
|
||||
description: No body
|
||||
---
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
|
||||
assert frontmatter["name"] == "minimal"
|
||||
assert body.strip() == ""
|
||||
Loading…
Add table
Add a link
Reference in a new issue