From 8f9232789118060dfed3752eea1e866827bc9b98 Mon Sep 17 00:00:00 2001 From: EloquentBrush0x <283442588+EloquentBrush0x@users.noreply.github.com> Date: Wed, 20 May 2026 22:40:31 +0300 Subject: [PATCH] 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. --- hermes_cli/skills_hub.py | 4 +-- tests/hermes_cli/test_skills_hub.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/hermes_cli/skills_hub.py b/hermes_cli/skills_hub.py index 01f19ba3b21..256624e53c9 100644 --- a/hermes_cli/skills_hub.py +++ b/hermes_cli/skills_hub.py @@ -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) diff --git a/tests/hermes_cli/test_skills_hub.py b/tests/hermes_cli/test_skills_hub.py index fa611e1a587..4d7cda80a72 100644 --- a/tests/hermes_cli/test_skills_hub.py +++ b/tests/hermes_cli/test_skills_hub.py @@ -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" + )