/ tests / agent / test_external_skills.py
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"]