fix(skills-hub): fix dedup in browse_skills() programmatic API

browse_skills() is the TUI gateway's API for the web UI skills browser
(tui_gateway/server.py:6574). It had the same dedup-by-name bug as
do_browse() and unified_search() fixed in the parent commit: r.name is
not unique for browse-sh skills (Airbnb, Booking.com, Zillow all publish
"search-listings"), so the dedup loop silently dropped all but the first
skill with each task name.

Switch to r.identifier, which is always globally unique.

Add a regression test asserting that two browse-sh skills with the same
name but different hostnames both appear in the browse_skills() result.
This commit is contained in:
EloquentBrush0x 2026-05-20 22:40:31 +03:00 committed by Teknium
parent fc7e04e9ed
commit 8f92327891
2 changed files with 41 additions and 2 deletions

View file

@ -704,8 +704,8 @@ def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> di
seen: dict = {}
for r in all_results:
rank = _TRUST_RANK.get(r.trust_level, 0)
if r.name not in seen or rank > _TRUST_RANK.get(seen[r.name].trust_level, 0):
seen[r.name] = r
if r.identifier not in seen or rank > _TRUST_RANK.get(seen[r.identifier].trust_level, 0):
seen[r.identifier] = r
deduped = list(seen.values())
deduped.sort(key=lambda r: (-_TRUST_RANK.get(r.trust_level, 0), r.source != "official", r.name.lower()))
total = len(deduped)

View file

@ -524,3 +524,42 @@ def test_existing_categories_returns_empty_when_skills_dir_missing(monkeypatch,
from hermes_cli.skills_hub import _existing_categories
assert _existing_categories() == []
# ---------------------------------------------------------------------------
# browse_skills — dedup by identifier, not name
# ---------------------------------------------------------------------------
def test_browse_skills_dedup_uses_identifier_not_name(monkeypatch):
"""browse_skills() must not collapse browse-sh skills that share a task name.
Airbnb and Booking.com both publish a 'search-listings' skill. Before the
fix, both were keyed by name so only one survived deduplication. After the
fix, each unique identifier produces a distinct result.
"""
from tools.skills_hub import SkillMeta
from hermes_cli.skills_hub import browse_skills
airbnb = SkillMeta(
name="search-listings", description="Airbnb search", source="browse-sh",
identifier="browse-sh/airbnb.com/search-listings-ddgioa", trust_level="community",
)
booking = SkillMeta(
name="search-listings", description="Booking.com search", source="browse-sh",
identifier="browse-sh/booking.com/search-listings-xyzab", trust_level="community",
)
mock_src = type("S", (), {
"source_id": lambda self: "browse-sh",
"search": lambda self, q, limit=500: [airbnb, booking],
})()
with patch("hermes_cli.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"]]
assert names.count("search-listings") == 2, (
"browse_skills() must not deduplicate browse-sh skills with the same name "
"but different identifiers"
)