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 == [] assert meta.user_invocable is True 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"], user_invocable=False, ) 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"] assert meta.user_invocable is False 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 grep", # type: ignore[arg-type] ) assert meta.allowed_tools == ["bash", "read", "grep"] def test_parses_allowed_tools_from_list(self) -> None: meta = SkillMetadata( name="test", description="A test skill", allowed_tools=["bash", "read"] ) assert meta.allowed_tools == ["bash", "read"] 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, prompt="Skill body") skill_dir = info.skill_dir assert info.name == "test-skill" assert info.description == "A test skill" assert info.license == "MIT" assert info.skill_path == skill_path.resolve() assert 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"], user_invocable=False, skill_path=skill_path, prompt="Skill body", ) 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.user_invocable is False 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, prompt="Skill body") assert info.skill_path is not None assert info.skill_path.is_absolute() assert info.skill_dir is not None 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"], user_invocable=False, ) info = SkillInfo.from_metadata(meta, skill_path, prompt="Skill body") assert info.license == meta.license assert info.compatibility == meta.compatibility assert info.metadata == meta.metadata assert info.allowed_tools == meta.allowed_tools assert info.user_invocable == meta.user_invocable def test_can_omit_skill_path_for_builtin_inline_prompt(self) -> None: info = SkillInfo( name="vibe", description="Builtin skill", prompt="Inline prompt" ) assert info.skill_path is None assert info.skill_dir is None