190 lines
8.0 KiB
Python
190 lines
8.0 KiB
Python
"""Tests for ``skills.create_dir`` — config-driven skill creation directory.
|
|
|
|
When configured, agent-created skills (skill_manage action=create) land in
|
|
``skills.create_dir`` instead of the profile-local skills dir, the directory
|
|
is scanned for discovery like the local dir, and the instruction text that
|
|
names the creation path renders the configured directory.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def isolated_home(tmp_path, monkeypatch):
|
|
"""Fresh HERMES_HOME with an empty local skills dir."""
|
|
home = tmp_path / ".hermes"
|
|
(home / "skills").mkdir(parents=True)
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
|
|
import hermes_constants
|
|
monkeypatch.setattr(hermes_constants, "_hermes_home_cache", None, raising=False)
|
|
|
|
from agent import skill_utils as su
|
|
su._external_dirs_cache_clear()
|
|
|
|
import tools.skills_tool as skills_tool
|
|
import tools.skill_manager_tool as smt
|
|
monkeypatch.setattr(skills_tool, "SKILLS_DIR", home / "skills")
|
|
monkeypatch.setattr(smt, "SKILLS_DIR", home / "skills")
|
|
yield home
|
|
su._external_dirs_cache_clear()
|
|
|
|
|
|
def _write_config(home: Path, body: str):
|
|
(home / "config.yaml").write_text(body, encoding="utf-8")
|
|
from agent import skill_utils as su
|
|
su._raw_config_cache_clear()
|
|
su._external_dirs_cache_clear()
|
|
|
|
|
|
def _skill_md(name: str) -> str:
|
|
return (
|
|
f"---\nname: {name}\n"
|
|
f"description: Use when testing create dir routing. One-line behavior.\n"
|
|
f"---\n\n# {name}\n\nBody.\n"
|
|
)
|
|
|
|
|
|
class TestGetSkillCreateDir:
|
|
def test_unset_returns_none(self, isolated_home):
|
|
from agent.skill_utils import get_skill_create_dir
|
|
_write_config(isolated_home, "skills:\n external_dirs: []\n")
|
|
assert get_skill_create_dir() is None
|
|
|
|
def test_absolute_path(self, isolated_home, tmp_path):
|
|
from agent.skill_utils import get_skill_create_dir
|
|
brain = tmp_path / "brain-skills"
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
assert get_skill_create_dir() == brain.resolve()
|
|
|
|
def test_relative_path_resolves_against_home(self, isolated_home):
|
|
from agent.skill_utils import get_skill_create_dir
|
|
_write_config(isolated_home, "skills:\n create_dir: brain\n")
|
|
assert get_skill_create_dir() == (isolated_home / "brain").resolve()
|
|
|
|
def test_tilde_expansion(self, isolated_home):
|
|
from agent.skill_utils import get_skill_create_dir
|
|
_write_config(isolated_home, "skills:\n create_dir: ~/brain-skills\n")
|
|
assert get_skill_create_dir() == (Path.home() / "brain-skills").resolve()
|
|
|
|
def test_local_skills_dir_treated_as_unset(self, isolated_home):
|
|
from agent.skill_utils import get_skill_create_dir
|
|
_write_config(
|
|
isolated_home, f"skills:\n create_dir: {isolated_home / 'skills'}\n"
|
|
)
|
|
assert get_skill_create_dir() is None
|
|
|
|
def test_empty_string_treated_as_unset(self, isolated_home):
|
|
from agent.skill_utils import get_skill_create_dir
|
|
_write_config(isolated_home, "skills:\n create_dir: ''\n")
|
|
assert get_skill_create_dir() is None
|
|
|
|
|
|
class TestDisplaySkillCreateDir:
|
|
def test_default_renders_local_skills_path(self, isolated_home):
|
|
from agent.skill_utils import display_skill_create_dir
|
|
_write_config(isolated_home, "skills: {}\n")
|
|
assert display_skill_create_dir().endswith("/skills/")
|
|
|
|
def test_configured_renders_configured_path(self, isolated_home, tmp_path):
|
|
from agent.skill_utils import display_skill_create_dir
|
|
brain = tmp_path / "opt-brain"
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
assert "opt-brain" in display_skill_create_dir()
|
|
|
|
def test_schema_helper_follows_config(self, isolated_home, tmp_path):
|
|
from tools.skill_manager_tool import _display_create_dir
|
|
brain = tmp_path / "opt-brain"
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
assert "opt-brain" in _display_create_dir()
|
|
|
|
|
|
class TestDiscovery:
|
|
def test_create_dir_in_all_skills_dirs(self, isolated_home, tmp_path):
|
|
from agent.skill_utils import get_all_skills_dirs
|
|
brain = tmp_path / "brain-skills"
|
|
brain.mkdir()
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
dirs = [d.resolve() for d in get_all_skills_dirs()]
|
|
assert dirs[0] == (isolated_home / "skills").resolve()
|
|
assert brain.resolve() in dirs
|
|
|
|
def test_missing_create_dir_not_scanned(self, isolated_home, tmp_path):
|
|
from agent.skill_utils import get_all_skills_dirs
|
|
brain = tmp_path / "does-not-exist"
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
assert brain.resolve() not in [d.resolve() for d in get_all_skills_dirs()]
|
|
|
|
def test_no_duplicate_when_also_in_external_dirs(self, isolated_home, tmp_path):
|
|
from agent.skill_utils import get_all_skills_dirs
|
|
brain = tmp_path / "brain-skills"
|
|
brain.mkdir()
|
|
_write_config(
|
|
isolated_home,
|
|
f"skills:\n create_dir: {brain}\n external_dirs:\n - {brain}\n",
|
|
)
|
|
dirs = [d.resolve() for d in get_all_skills_dirs()]
|
|
assert dirs.count(brain.resolve()) == 1
|
|
|
|
|
|
class TestCreateRouting:
|
|
def test_create_lands_in_create_dir(self, isolated_home, tmp_path):
|
|
from tools.skill_manager_tool import skill_manage
|
|
brain = tmp_path / "brain-skills"
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
res = json.loads(skill_manage("", "", operations=[{
|
|
"action": "create", "name": "routed-skill",
|
|
"content": _skill_md("routed-skill"),
|
|
}]))
|
|
assert res.get("success"), res
|
|
assert (brain / "routed-skill" / "SKILL.md").exists()
|
|
assert not (isolated_home / "skills" / "routed-skill").exists()
|
|
# Out-of-root creation reports an absolute path, not a relative_to
|
|
# crash (single-op legacy shape surfaces the path field).
|
|
res_flat = json.loads(skill_manage(
|
|
"create", "routed-skill-flat", content=_skill_md("routed-skill-flat"),
|
|
))
|
|
assert res_flat.get("success"), res_flat
|
|
assert str(brain / "routed-skill-flat") == res_flat["path"]
|
|
|
|
def test_create_with_category(self, isolated_home, tmp_path):
|
|
from tools.skill_manager_tool import skill_manage
|
|
brain = tmp_path / "brain-skills"
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
res = json.loads(skill_manage("", "", operations=[{
|
|
"action": "create", "name": "cat-skill", "category": "devops",
|
|
"content": _skill_md("cat-skill"),
|
|
}]))
|
|
assert res.get("success"), res
|
|
assert (brain / "devops" / "cat-skill" / "SKILL.md").exists()
|
|
|
|
def test_default_create_still_local(self, isolated_home):
|
|
from tools.skill_manager_tool import skill_manage
|
|
_write_config(isolated_home, "skills: {}\n")
|
|
res = json.loads(skill_manage("", "", operations=[{
|
|
"action": "create", "name": "local-skill",
|
|
"content": _skill_md("local-skill"),
|
|
}]))
|
|
assert res.get("success"), res
|
|
assert (isolated_home / "skills" / "local-skill" / "SKILL.md").exists()
|
|
|
|
def test_created_skill_is_findable_and_patchable(self, isolated_home, tmp_path):
|
|
from tools.skill_manager_tool import skill_manage, _find_skill
|
|
brain = tmp_path / "brain-skills"
|
|
_write_config(isolated_home, f"skills:\n create_dir: {brain}\n")
|
|
json.loads(skill_manage("", "", operations=[{
|
|
"action": "create", "name": "patchable-skill",
|
|
"content": _skill_md("patchable-skill"),
|
|
}]))
|
|
found = _find_skill("patchable-skill")
|
|
assert found is not None
|
|
res = json.loads(skill_manage("", "", operations=[{
|
|
"action": "patch", "name": "patchable-skill",
|
|
"old_string": "Body.", "new_string": "Patched.",
|
|
}]))
|
|
assert res.get("success"), res
|
|
assert "Patched." in (brain / "patchable-skill" / "SKILL.md").read_text()
|