test_external_skills.py
1 """Tests for external skill directories (skills.external_dirs config).""" 2 3 import json 4 import os 5 from pathlib import Path 6 from unittest.mock import patch 7 8 import pytest 9 10 11 @pytest.fixture 12 def external_skills_dir(tmp_path): 13 """Create a temp dir with a sample external skill.""" 14 ext_dir = tmp_path / "external-skills" 15 skill_dir = ext_dir / "my-external-skill" 16 skill_dir.mkdir(parents=True) 17 (skill_dir / "SKILL.md").write_text( 18 "---\nname: my-external-skill\ndescription: A skill from an external directory\n---\n\n# My External Skill\n\nDo external things.\n" 19 ) 20 return ext_dir 21 22 23 @pytest.fixture 24 def hermes_home(tmp_path): 25 """Create a minimal HERMES_HOME with config.""" 26 home = tmp_path / ".hermes" 27 home.mkdir() 28 (home / "skills").mkdir() 29 return home 30 31 32 class TestGetExternalSkillsDirs: 33 def test_empty_config(self, hermes_home): 34 (hermes_home / "config.yaml").write_text("skills:\n external_dirs: []\n") 35 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 36 from agent.skill_utils import get_external_skills_dirs 37 result = get_external_skills_dirs() 38 assert result == [] 39 40 def test_nonexistent_dir_skipped(self, hermes_home): 41 (hermes_home / "config.yaml").write_text( 42 "skills:\n external_dirs:\n - /nonexistent/path\n" 43 ) 44 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 45 from agent.skill_utils import get_external_skills_dirs 46 result = get_external_skills_dirs() 47 assert result == [] 48 49 def test_valid_dir_returned(self, hermes_home, external_skills_dir): 50 (hermes_home / "config.yaml").write_text( 51 f"skills:\n external_dirs:\n - {external_skills_dir}\n" 52 ) 53 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 54 from agent.skill_utils import get_external_skills_dirs 55 result = get_external_skills_dirs() 56 assert len(result) == 1 57 assert result[0] == external_skills_dir.resolve() 58 59 def test_duplicate_dirs_deduplicated(self, hermes_home, external_skills_dir): 60 (hermes_home / "config.yaml").write_text( 61 f"skills:\n external_dirs:\n - {external_skills_dir}\n - {external_skills_dir}\n" 62 ) 63 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 64 from agent.skill_utils import get_external_skills_dirs 65 result = get_external_skills_dirs() 66 assert len(result) == 1 67 68 def test_local_skills_dir_excluded(self, hermes_home): 69 local_skills = hermes_home / "skills" 70 (hermes_home / "config.yaml").write_text( 71 f"skills:\n external_dirs:\n - {local_skills}\n" 72 ) 73 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 74 from agent.skill_utils import get_external_skills_dirs 75 result = get_external_skills_dirs() 76 assert result == [] 77 78 def test_no_config_file(self, hermes_home): 79 # No config.yaml at all 80 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 81 from agent.skill_utils import get_external_skills_dirs 82 result = get_external_skills_dirs() 83 assert result == [] 84 85 def test_string_value_converted_to_list(self, hermes_home, external_skills_dir): 86 (hermes_home / "config.yaml").write_text( 87 f"skills:\n external_dirs: {external_skills_dir}\n" 88 ) 89 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 90 from agent.skill_utils import get_external_skills_dirs 91 result = get_external_skills_dirs() 92 assert len(result) == 1 93 94 95 class TestGetAllSkillsDirs: 96 def test_local_always_first(self, hermes_home, external_skills_dir): 97 (hermes_home / "config.yaml").write_text( 98 f"skills:\n external_dirs:\n - {external_skills_dir}\n" 99 ) 100 with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): 101 from agent.skill_utils import get_all_skills_dirs 102 result = get_all_skills_dirs() 103 assert result[0] == hermes_home / "skills" 104 assert result[1] == external_skills_dir.resolve() 105 106 107 class TestExternalSkillsInFindAll: 108 def test_external_skills_found(self, hermes_home, external_skills_dir): 109 (hermes_home / "config.yaml").write_text( 110 f"skills:\n external_dirs:\n - {external_skills_dir}\n" 111 ) 112 local_skills = hermes_home / "skills" 113 with ( 114 patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}), 115 patch("tools.skills_tool.SKILLS_DIR", local_skills), 116 ): 117 from tools.skills_tool import _find_all_skills 118 skills = _find_all_skills() 119 names = [s["name"] for s in skills] 120 assert "my-external-skill" in names 121 122 def test_local_takes_precedence(self, hermes_home, external_skills_dir): 123 """If the same skill name exists locally and externally, local wins.""" 124 local_skills = hermes_home / "skills" 125 local_skill = local_skills / "my-external-skill" 126 local_skill.mkdir(parents=True) 127 (local_skill / "SKILL.md").write_text( 128 "---\nname: my-external-skill\ndescription: Local version\n---\n\nLocal.\n" 129 ) 130 (hermes_home / "config.yaml").write_text( 131 f"skills:\n external_dirs:\n - {external_skills_dir}\n" 132 ) 133 with ( 134 patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}), 135 patch("tools.skills_tool.SKILLS_DIR", local_skills), 136 ): 137 from tools.skills_tool import _find_all_skills 138 skills = _find_all_skills() 139 matching = [s for s in skills if s["name"] == "my-external-skill"] 140 assert len(matching) == 1 141 assert matching[0]["description"] == "Local version" 142 143 144 class TestExternalSkillView: 145 def test_skill_view_finds_external(self, hermes_home, external_skills_dir): 146 (hermes_home / "config.yaml").write_text( 147 f"skills:\n external_dirs:\n - {external_skills_dir}\n" 148 ) 149 local_skills = hermes_home / "skills" 150 with ( 151 patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}), 152 patch("tools.skills_tool.SKILLS_DIR", local_skills), 153 ): 154 from tools.skills_tool import skill_view 155 result = json.loads(skill_view("my-external-skill")) 156 assert result["success"] is True 157 assert "external things" in result["content"]