Files
hermes-agent/tests/tools/test_skills_hub_generic_dir.py
Teknium 429f966018 fix(skills): one install name per skills.sh skill; never swap a gone GitHub path for another skill
Review fixes for the generic-dir fallback:
- SkillsShSource.fetch now names a generic-dir bundle after the slug too, so a skills.sh install
  that bypasses the index no longer lands in a shared skills/ dir.
- HermesIndexSource.fetch applies the slug name whether the row resolved via resolved_github_id
  or fell back to the tree, so a rebuilt index does not create a second lock entry ('skills').
- The tree fallback only runs for skills.sh rows: a deleted concrete GitHub path stays unavailable
  instead of installing whichever skill remains in that repo under the requested name.
- _find_skill_in_repo_tree's lone-SKILL.md fallback only accepts a generic dir (skills,
  .agents/skills, .claude/skills); a lone named skill (skills/bar) no longer answers for slug foo.
- The PR's two tests move to tests/tools/test_skills_hub_generic_dir.py (test_skills_hub.py is at its
  code-health line cap).
2026-10-05 15:56:28 -07:00

69 lines
3.6 KiB
Python

"""Skills hub: a skills.sh skill kept in a generic dir (``skills/SKILL.md``) resolves and installs under its slug."""
from unittest.mock import MagicMock, patch
from tools.skills_hub_github import GitHubAuth, GitHubSource
from tools.skills_hub_models import SkillBundle
from tools.skills_hub_official import HermesIndexSource
from tools.skills_hub_skillssh import SkillsShSource
ROW = {"name": "weread-skills", "description": "WeChat Reading skills", "source": "skills.sh",
"identifier": "skills-sh/tencent/wechatreading/weread-skills",
"repo": "tencent/wechatreading", "path": "weread-skills"}
def _generic_bundle(*_):
return SkillBundle(name="skills", files={"SKILL.md": "---\nname: weread-skills\n---\n"},
source="github", identifier="tencent/wechatreading/skills", trust_level="community")
def _index_source(row, github, monkeypatch):
src = HermesIndexSource(auth=GitHubAuth())
src._index, src._loaded = {"skills": [row]}, True
monkeypatch.setattr(src, "_get_github", lambda: github)
return src
@patch("tools.skills_hub.httpx.get")
def test_finds_single_generic_skills_directory_when_slug_is_stale(mock_get):
"""A lone ``skills/SKILL.md`` answers for a stale slug; a lone NAMED skill (``skills/bar``) never does."""
auth = MagicMock(spec=GitHubAuth)
auth.get_headers.return_value = {"Accept": "application/vnd.github.v3+json"}
for lone, expected in (("skills/SKILL.md", "tencent/wechatreading/skills"), ("skills/bar/SKILL.md", None)):
tree_entries = [{"path": "README.md", "type": "blob"}, {"path": lone, "type": "blob"}]
mock_get.side_effect = [
MagicMock(status_code=200, json=lambda: {"default_branch": "main"}),
MagicMock(status_code=200, json=lambda t=tree_entries: {"tree": t}),
]
assert GitHubSource(auth=auth)._find_skill_in_repo_tree("tencent/wechatreading", "weread-skills") == expected
def test_fetch_resolves_generic_skill_directory_from_index_slug(monkeypatch):
"""A skills.sh skill under a generic ``skills/`` dir installs as its slug, whether the index fell
back to the tree, resolved it, or skills.sh fetched it directly; a gone GitHub path stays gone."""
github = MagicMock()
github._find_skill_in_repo_tree.return_value = "tencent/wechatreading/skills"
github._find_repo_root_skill.return_value = None
for extra, fetched in (({}, [None, _generic_bundle()]),
({"resolved_github_id": "tencent/wechatreading/skills"}, [_generic_bundle()])):
github.fetch.side_effect = fetched
bundle = _index_source({**ROW, **extra}, github, monkeypatch).fetch(ROW["identifier"])
assert (bundle.name, bundle.identifier) == ("weread-skills", ROW["identifier"]) # one lock name
github._find_skill_in_repo_tree.assert_called_once_with("tencent/wechatreading", "weread-skills")
skills_sh = SkillsShSource(auth=MagicMock(spec=GitHubAuth))
monkeypatch.setattr(skills_sh, "_fetch_detail_page", lambda _id: None)
monkeypatch.setattr(skills_sh, "_candidate_identifiers", lambda _id: [])
monkeypatch.setattr(skills_sh, "_discover_identifier", lambda *_a, **_k: "tencent/wechatreading/skills")
monkeypatch.setattr(skills_sh.github, "fetch", _generic_bundle)
assert skills_sh.fetch(ROW["identifier"]).name == "weread-skills"
github.reset_mock()
github.fetch.side_effect = [None]
gone = {**ROW, "source": "github", "identifier": "tencent/wechatreading/gone", "path": "gone"}
assert _index_source(gone, github, monkeypatch).fetch(gone["identifier"]) is None # no other skill relabeled
github._find_skill_in_repo_tree.assert_not_called()