273 lines
8.6 KiB
Python
273 lines
8.6 KiB
Python
"""
|
|
Tests for the Skills System.
|
|
"""
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from skills.skills import Skill, SkillsManager
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def skills_workspace(tmp_path):
|
|
"""Create a temp workspace with sample skills."""
|
|
skills_dir = tmp_path / "skills"
|
|
|
|
# Skill 1: weather
|
|
weather_dir = skills_dir / "weather"
|
|
weather_dir.mkdir(parents=True)
|
|
(weather_dir / "SKILL.md").write_text(
|
|
"""---
|
|
name: weather
|
|
description: Check weather for any city
|
|
triggers: [weather, forecast, temperature, rain]
|
|
---
|
|
|
|
# Weather Skill
|
|
|
|
When the user asks about weather:
|
|
1. Extract the city name
|
|
2. Provide info based on your knowledge
|
|
"""
|
|
)
|
|
|
|
# Skill 2: coding
|
|
coding_dir = skills_dir / "coding"
|
|
coding_dir.mkdir(parents=True)
|
|
(coding_dir / "SKILL.md").write_text(
|
|
"""---
|
|
name: coding
|
|
description: Help with programming tasks
|
|
triggers: [code, python, javascript, bug, debug]
|
|
---
|
|
|
|
# Coding Skill
|
|
|
|
When the user asks about coding:
|
|
- Ask what language they're working in
|
|
- Provide code snippets with explanations
|
|
"""
|
|
)
|
|
|
|
# Skill 3: invalid (no SKILL.md)
|
|
invalid_dir = skills_dir / "empty_skill"
|
|
invalid_dir.mkdir(parents=True)
|
|
# No SKILL.md here
|
|
|
|
# Skill 4: minimal (no explicit triggers)
|
|
minimal_dir = skills_dir / "minimal"
|
|
minimal_dir.mkdir(parents=True)
|
|
(minimal_dir / "SKILL.md").write_text(
|
|
"""---
|
|
name: minimal_skill
|
|
description: A minimal skill
|
|
---
|
|
|
|
This is a minimal skill with no explicit triggers.
|
|
"""
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
|
|
@pytest.fixture
|
|
def empty_workspace(tmp_path):
|
|
"""Create a temp workspace with no skills directory."""
|
|
return tmp_path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: Skill Dataclass
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkill:
|
|
def test_matches_trigger(self):
|
|
skill = Skill(
|
|
name="weather",
|
|
description="Weather skill",
|
|
triggers=["weather", "forecast"],
|
|
body="# Weather",
|
|
path="/fake/path",
|
|
)
|
|
assert skill.matches("What's the weather today?")
|
|
assert skill.matches("Give me the FORECAST")
|
|
assert not skill.matches("Tell me a joke")
|
|
|
|
def test_matches_is_case_insensitive(self):
|
|
skill = Skill(
|
|
name="test",
|
|
description="Test",
|
|
triggers=["Python"],
|
|
body="body",
|
|
path="/fake",
|
|
)
|
|
assert skill.matches("I'm learning python")
|
|
assert skill.matches("PYTHON is great")
|
|
assert skill.matches("Python 3.14")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: SkillsManager Loading
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillsManagerLoading:
|
|
def test_load_all_finds_skills(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
loaded = manager.load_all()
|
|
# Should find weather, coding, minimal (not empty_skill which has no SKILL.md)
|
|
assert len(loaded) == 3
|
|
names = {s.name for s in loaded}
|
|
assert "weather" in names
|
|
assert "coding" in names
|
|
assert "minimal_skill" in names
|
|
|
|
def test_load_parses_frontmatter(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
weather = next(s for s in manager.skills if s.name == "weather")
|
|
assert weather.description == "Check weather for any city"
|
|
assert "weather" in weather.triggers
|
|
assert "forecast" in weather.triggers
|
|
assert "temperature" in weather.triggers
|
|
assert "rain" in weather.triggers
|
|
|
|
def test_load_parses_body(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
weather = next(s for s in manager.skills if s.name == "weather")
|
|
assert "# Weather Skill" in weather.body
|
|
assert "Extract the city name" in weather.body
|
|
|
|
def test_empty_workspace(self, empty_workspace):
|
|
manager = SkillsManager(str(empty_workspace))
|
|
loaded = manager.load_all()
|
|
assert loaded == []
|
|
|
|
def test_minimal_skill_uses_name_as_trigger(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
minimal = next(s for s in manager.skills if s.name == "minimal_skill")
|
|
assert minimal.triggers == ["minimal_skill"]
|
|
|
|
def test_reload_rediscovers(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
assert len(manager.skills) == 3
|
|
|
|
# Add a new skill
|
|
new_dir = skills_workspace / "skills" / "newskill"
|
|
new_dir.mkdir()
|
|
(new_dir / "SKILL.md").write_text(
|
|
"---\nname: newskill\ndescription: New\ntriggers: [new]\n---\nNew body"
|
|
)
|
|
|
|
reloaded = manager.reload()
|
|
assert len(reloaded) == 4
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: SkillsManager Matching
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillsManagerMatching:
|
|
def test_get_relevant_finds_matching(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
relevant = manager.get_relevant("What's the weather?")
|
|
names = {s.name for s in relevant}
|
|
assert "weather" in names
|
|
assert "coding" not in names
|
|
|
|
def test_get_relevant_multiple_matches(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
# This should match both weather (temperature) and coding (debug)
|
|
relevant = manager.get_relevant("debug the temperature sensor code")
|
|
names = {s.name for s in relevant}
|
|
assert "weather" in names
|
|
assert "coding" in names
|
|
|
|
def test_get_relevant_no_matches(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
relevant = manager.get_relevant("Tell me a joke about cats")
|
|
assert relevant == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: Context Generation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillsManagerContext:
|
|
def test_get_context_with_matching(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
context = manager.get_context("forecast for tomorrow")
|
|
assert "# Active Skills" in context
|
|
assert "Weather Skill" in context
|
|
|
|
def test_get_context_empty_when_no_match(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
context = manager.get_context("random unrelated message")
|
|
assert context == ""
|
|
|
|
def test_get_all_context(self, skills_workspace):
|
|
manager = SkillsManager(str(skills_workspace))
|
|
manager.load_all()
|
|
summary = manager.get_all_context()
|
|
assert "# Available Skills" in summary
|
|
assert "weather" in summary
|
|
assert "coding" in summary
|
|
|
|
def test_get_all_context_empty_workspace(self, empty_workspace):
|
|
manager = SkillsManager(str(empty_workspace))
|
|
manager.load_all()
|
|
assert manager.get_all_context() == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: Frontmatter Parsing Edge Cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFrontmatterParsing:
|
|
def test_parse_list_inline_brackets(self):
|
|
result = SkillsManager._parse_list("[a, b, c]")
|
|
assert result == ["a", "b", "c"]
|
|
|
|
def test_parse_list_no_brackets(self):
|
|
result = SkillsManager._parse_list("a, b, c")
|
|
assert result == ["a", "b", "c"]
|
|
|
|
def test_parse_list_quoted_items(self):
|
|
result = SkillsManager._parse_list("['hello', \"world\"]")
|
|
assert result == ["hello", "world"]
|
|
|
|
def test_parse_list_empty(self):
|
|
result = SkillsManager._parse_list("")
|
|
assert result == []
|
|
|
|
def test_split_frontmatter(self):
|
|
content = "---\nname: test\n---\nBody here"
|
|
fm, body = SkillsManager._split_frontmatter(content)
|
|
assert "name: test" in fm
|
|
assert body == "Body here"
|
|
|
|
def test_split_no_frontmatter(self):
|
|
content = "Just a body with no frontmatter"
|
|
fm, body = SkillsManager._split_frontmatter(content)
|
|
assert fm == ""
|
|
assert body == content
|