"""Tests for SkillLoader."""
import pytest
class TestSkillLoader:
"""Tests for SkillLoader skill management."""
def test_list_skills_returns_list(self, tmp_path):
"""SkillLoader.list_skills returns a list."""
from src.localvoicemode.skills.loader import SkillLoader
# Create a minimal skill directory
skill_dir = tmp_path / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: test-skill
name: Test
display_name: Test Skill
description: A test skill
---
# Test Skill
You are a test assistant.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skills = loader.list_skills()
assert isinstance(skills, list)
assert len(skills) >= 1
def test_list_skills_returns_empty_for_missing_dir(self, tmp_path):
"""SkillLoader.list_skills returns empty list if skills dir doesn't exist."""
from src.localvoicemode.skills.loader import SkillLoader
nonexistent = tmp_path / "nonexistent"
loader = SkillLoader(nonexistent, tmp_path)
skills = loader.list_skills()
assert skills == []
def test_load_skill_returns_skill_object(self, tmp_path, sample_skill_data):
"""SkillLoader.load_skill returns a Skill object."""
from src.localvoicemode.skills.loader import SkillLoader
# Create a minimal skill directory
skill_dir = tmp_path / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: test-skill
name: Test
display_name: Test Skill
description: A test skill
---
# Test Skill
You are a test assistant.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill = loader.load_skill("test-skill")
assert skill is not None
assert skill.id == "test-skill"
def test_load_skill_caches_result(self, tmp_path):
"""SkillLoader caches loaded skills."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "cached-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: cached-skill
name: Cached
display_name: Cached Skill
description: A cached skill
---
You are cached.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill1 = loader.load_skill("cached-skill")
skill2 = loader.load_skill("cached-skill")
assert skill1 is skill2 # Same object from cache
def test_load_skill_returns_none_for_missing(self, tmp_path):
"""SkillLoader.load_skill returns None for nonexistent skill."""
from src.localvoicemode.skills.loader import SkillLoader
loader = SkillLoader(tmp_path, tmp_path)
skill = loader.load_skill("nonexistent-skill")
assert skill is None
def test_load_skill_parses_all_fields(self, tmp_path):
"""SkillLoader parses all SKILL.md fields."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "full-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: full-skill
name: Full Skill
display_name: Full Test Skill
description: A skill with all fields
emoji: "!"
metadata:
setting: Test setting
greeting: Hello there!
initial_memories:
- Memory 1
- Memory 2
personality_traits:
- helpful
- friendly
speech_patterns:
- speaks clearly
allowed_tools:
- tool1
- tool2
---
# Full Skill
You are a full test skill with all fields populated.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill = loader.load_skill("full-skill")
assert skill is not None
assert skill.id == "full-skill"
assert skill.name == "Full Skill"
assert skill.display_name == "Full Test Skill"
assert skill.description == "A skill with all fields"
assert skill.emoji == "!"
assert skill.metadata == {"setting": "Test setting", "greeting": "Hello there!"}
assert skill.initial_memories == ["Memory 1", "Memory 2"]
assert skill.personality_traits == ["helpful", "friendly"]
assert skill.speech_patterns == ["speaks clearly"]
assert skill.allowed_tools == ["tool1", "tool2"]
assert "full test skill" in skill.system_prompt.lower()
def test_clear_cache(self, tmp_path):
"""SkillLoader.clear_cache clears the cache."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "cache-test"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: cache-test
name: Cache Test
display_name: Cache Test
description: Testing cache clear
---
Test skill.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill1 = loader.load_skill("cache-test")
loader.clear_cache()
skill2 = loader.load_skill("cache-test")
assert skill1 is not skill2 # Different objects after cache clear
def test_list_skills_extracts_metadata(self, tmp_path):
"""list_skills extracts id, name, display_name, description, emoji."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "metadata-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: metadata-skill
name: Metadata Skill
display_name: Metadata Display Name
description: A skill for testing metadata extraction
emoji: "*"
---
System prompt here.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skills = loader.list_skills()
assert len(skills) == 1
skill_info = skills[0]
assert skill_info["id"] == "metadata-skill"
assert skill_info["name"] == "Metadata Skill"
assert skill_info["display_name"] == "Metadata Display Name"
assert skill_info["description"] == "A skill for testing metadata extraction"
assert skill_info["emoji"] == "*"
def test_load_skill_parses_output_filters(self, tmp_path):
"""SkillLoader parses output_filters from SKILL.md."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "filter-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: filter-skill
name: Filter Skill
display_name: Filter Skill
description: A skill with output filters
output_filters:
- roleplay
- tts
---
You are a filtered skill.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill = loader.load_skill("filter-skill")
assert skill is not None
assert skill.output_filters == ["roleplay", "tts"]
def test_load_skill_parses_input_validation(self, tmp_path):
"""SkillLoader parses input_validation from SKILL.md."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "validation-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: validation-skill
name: Validation Skill
display_name: Validation Skill
description: A skill with input validation
input_validation:
block_injection: true
---
You are a validated skill.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill = loader.load_skill("validation-skill")
assert skill is not None
assert skill.input_validation == {"block_injection": True}
def test_load_skill_defaults_empty_output_filters(self, tmp_path):
"""SkillLoader defaults output_filters to empty list."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "no-filters"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: no-filters
name: No Filters
display_name: No Filters
description: A skill without filters
---
You have no filters.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill = loader.load_skill("no-filters")
assert skill is not None
assert skill.output_filters == []
assert skill.input_validation == {}
def test_load_skill_parses_combined_filter_config(self, tmp_path):
"""SkillLoader parses both output_filters and input_validation together."""
from src.localvoicemode.skills.loader import SkillLoader
skill_dir = tmp_path / "combined-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
id: combined-skill
name: Combined Skill
display_name: Combined Skill
description: A skill with all filter config
output_filters:
- roleplay
- tts
input_validation:
block_injection: true
strict_mode: false
---
You have combined configuration.
"""
)
loader = SkillLoader(tmp_path, tmp_path)
skill = loader.load_skill("combined-skill")
assert skill is not None
assert skill.output_filters == ["roleplay", "tts"]
assert skill.input_validation == {"block_injection": True, "strict_mode": False}