Files
Aetheel/tests/test_skills.py
Tanmay Karande 41b2f9a593 latest updates
2026-02-15 15:02:58 -05:00

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