diff --git a/tests/hermes_cli/test_skills_hub.py b/tests/hermes_cli/test_skills_hub.py index 4d7cda80a72..1eca264b12c 100644 --- a/tests/hermes_cli/test_skills_hub.py +++ b/tests/hermes_cli/test_skills_hub.py @@ -555,7 +555,9 @@ def test_browse_skills_dedup_uses_identifier_not_name(monkeypatch): "search": lambda self, q, limit=500: [airbnb, booking], })() - with patch("hermes_cli.skills_hub.create_source_router", return_value=[mock_src]): + # browse_skills() imports create_source_router locally from tools.skills_hub, + # so the patch must target the source module, not hermes_cli.skills_hub. + with patch("tools.skills_hub.create_source_router", return_value=[mock_src]): result = browse_skills(page=1, page_size=50) names = [item["name"] for item in result["items"]] diff --git a/tools/skills_hub.py b/tools/skills_hub.py index 9e808b09277..12372e34ce6 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -379,14 +379,16 @@ class GitHubSource(SkillSource): logger.debug(f"Failed to search {tap['repo']}: {e}") continue - # Deduplicate by name, preferring higher trust levels + # Deduplicate by identifier, preferring higher trust levels. + # identifier is unique per skill; name is not (two configured taps can + # publish skills with the same name but different identifiers). _trust_rank = {"builtin": 2, "trusted": 1, "community": 0} seen = {} for r in results: - if r.name not in seen: - seen[r.name] = r - elif _trust_rank.get(r.trust_level, 0) > _trust_rank.get(seen[r.name].trust_level, 0): - seen[r.name] = r + if r.identifier not in seen: + seen[r.identifier] = r + elif _trust_rank.get(r.trust_level, 0) > _trust_rank.get(seen[r.identifier].trust_level, 0): + seen[r.identifier] = r results = list(seen.values()) return results[:limit]