""" 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