fix(skills-hub): stop ellipsis-truncating the Identifier column (#33810)

`hermes skills search` rendered the Identifier column with the default
overflow behaviour, so long slugs (notably browse-sh — every browse-sh
skill ends in a `-XXXXXX` hash that's part of the identifier) were cut
to `browse-sh/weathe…`. Users copied the visible string into
`hermes skills install` and got a not-found error because the hash was
gone.

Set overflow="fold" on the Identifier column in both search tables
(`do_search` and the `_resolve_short_name` multi-match table) so long
slugs wrap onto a second line instead of getting eaten. Also add a
`--json` flag to `hermes skills search` (and the `/skills search`
slash variant) for scripting — emits a list of {name, identifier,
source, trust_level, description} objects with the full identifier,
which is the right shape for copy-paste pipelines too.

Closes #33674.
This commit is contained in:
Teknium 2026-05-28 04:53:13 -07:00 committed by GitHub
parent 5e1f793430
commit e0572a6def
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 143 additions and 9 deletions

View file

@ -651,3 +651,95 @@ def test_browse_skills_dedup_uses_identifier_not_name(monkeypatch):
"browse_skills() must not deduplicate browse-sh skills with the same name "
"but different identifiers"
)
# ---------------------------------------------------------------------------
# Regression: full identifier must be recoverable from `hermes skills search`
# even when the slug is too long to fit the terminal width (issue #33674).
# ---------------------------------------------------------------------------
# A real browse-sh-style slug whose trailing -XXXXXX hash matters for install
_LONG_SLUG = "browse-sh/weather.gov/get-forecast-1uezib"
_LONG_RESULT = type("R", (), {
"name": "get-forecast",
"description": "Fetch the forecast",
"source": "browse-sh",
"trust_level": "community",
"identifier": _LONG_SLUG,
})()
def test_do_search_identifier_column_does_not_truncate_long_slug():
"""The Identifier column must use overflow='fold', not the default ellipsis.
Renders into a deliberately narrow Console; the full slug (including the
trailing -1uezib hash) must still appear in the output. Before the fix,
Rich would render `browse-sh/weather` and lose the hash.
"""
from hermes_cli.skills_hub import do_search
sink = StringIO()
# Narrow width forces Rich to apply overflow rules — exactly the scenario
# the issue reports. width=40 is too small for the slug; we want the slug
# wrapped (not ellipsis-truncated).
console = Console(file=sink, force_terminal=False, color_system=None, width=40)
with patch("tools.skills_hub.unified_search", return_value=[_LONG_RESULT]), \
patch("tools.skills_hub.create_source_router", return_value={}), \
patch("tools.skills_hub.GitHubAuth"):
do_search("weather", console=console)
output = sink.getvalue()
# The fix is working when the Identifier column wraps the slug across
# multiple lines (folded chunks) rather than emitting ONE line with an
# ellipsis. Extract every chunk that appears in the rightmost cell of
# the table by walking lines that look like table rows ("│ ... │") and
# taking the last `│...│` cell. Concatenating those chunks must yield
# the full slug.
chunks = []
for line in output.splitlines():
# Table data rows start and end with the box-drawing vertical bar.
if not line.startswith("") or not line.rstrip().endswith(""):
continue
# Last `│ ... │` cell on the row is the Identifier column.
last_cell = line.rstrip().rsplit("", 2)[-2].strip()
if last_cell:
chunks.append(last_cell)
reconstructed = "".join(chunks)
assert _LONG_SLUG in reconstructed, (
f"Expected full slug {_LONG_SLUG!r} to be recoverable from the "
f"folded Identifier column; got chunks {chunks!r}\n"
f"Full output:\n{output}"
)
# And the truncating ellipsis must NOT appear in the Identifier column.
# Rich uses U+2026 HORIZONTAL ELLIPSIS for the default overflow="ellipsis".
assert "\u2026" not in reconstructed, (
f"Identifier column still ellipsis-truncated: {reconstructed!r}"
)
def test_do_search_json_flag_emits_full_identifiers(capsys):
"""`--json` must print a parseable array with full identifiers and skip the table."""
from hermes_cli.skills_hub import do_search
sink = StringIO()
console = Console(file=sink, force_terminal=False, color_system=None, width=40)
with patch("tools.skills_hub.unified_search", return_value=[_LONG_RESULT]), \
patch("tools.skills_hub.create_source_router", return_value={}), \
patch("tools.skills_hub.GitHubAuth"):
do_search("weather", console=console, as_json=True)
# JSON goes to stdout via print(), not the Rich console sink.
captured = capsys.readouterr().out
import json as _json
payload = _json.loads(captured)
assert isinstance(payload, list) and len(payload) == 1
assert payload[0]["identifier"] == _LONG_SLUG
assert payload[0]["name"] == "get-forecast"
assert payload[0]["source"] == "browse-sh"
# Table render must be suppressed — sink should be empty (no "Searching for:" header).
assert "Searching for:" not in sink.getvalue()