latest updates
This commit is contained in:
272
tests/test_skills.py
Normal file
272
tests/test_skills.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user