Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions hermes_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,13 @@ def discord_skill_commands_by_category(
_SLACK_MAX_SLASH_COMMANDS = 50
_SLACK_NAME_LIMIT = 32
_SLACK_INVALID_CHARS = re.compile(r"[^a-z0-9_\-]")
_SLACK_RESERVED_COMMANDS = frozenset({
# Built-in Slack slash commands that cannot be registered by apps.
# https://slack.com/help/articles/201259356-Use-built-in-slash-commands
"me", "status", "away", "dnd", "shrug", "remind", "msg", "feed",
"who", "collapse", "expand", "leave", "join", "open", "search",
"topic", "mute", "pro", "shortcuts",
})


def _sanitize_slack_name(raw: str) -> str:
Expand All @@ -864,6 +871,10 @@ def slack_native_slashes() -> list[tuple[str, str, str]]:
documented form (e.g. ``/background``, ``/bg``, and ``/btw`` all work).
Plugin-registered slash commands are included too.

Commands whose sanitized name collides with a Slack built-in
(e.g. ``/status``, ``/me``, ``/join``) are silently skipped. Users
can still reach them via ``/hermes <command>``.

Results are clamped to Slack's 50-command limit with duplicate-name
avoidance. ``/hermes`` is always reserved as the first entry so the
legacy ``/hermes <subcommand>`` form keeps working for anything that
Expand All @@ -881,6 +892,8 @@ def _add(name: str, desc: str, hint: str) -> None:
slack_name = _sanitize_slack_name(name)
if not slack_name or slack_name in seen:
return
if slack_name in _SLACK_RESERVED_COMMANDS:
return
if len(entries) >= _SLACK_MAX_SLASH_COMMANDS:
return
# Slack description cap is 2000 chars; keep it short.
Expand Down
19 changes: 17 additions & 2 deletions tests/hermes_cli/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
SlashCommandAutoSuggest,
SlashCommandCompleter,
_CMD_NAME_LIMIT,
_SLACK_RESERVED_COMMANDS,
_TG_NAME_LIMIT,
_clamp_command_names,
_clamp_telegram_names,
Expand Down Expand Up @@ -299,9 +300,19 @@ def test_unique_names(self):
def test_includes_canonical_commands(self):
names = {n for n, _d, _h in slack_native_slashes()}
# Sample of gateway-available canonical commands
for expected in ("new", "stop", "background", "model", "help", "status"):
for expected in ("new", "stop", "background", "model", "help"):
assert expected in names, f"missing canonical /{expected}"

def test_excludes_slack_reserved_commands(self):
"""Slack built-in commands (e.g. /status, /me, /join) cannot be
registered by apps and must be excluded from the manifest.
Users can still reach them via /hermes <command>."""
names = {n for n, _d, _h in slack_native_slashes()}
for reserved in _SLACK_RESERVED_COMMANDS:
assert reserved not in names, (
f"/{reserved} is a Slack built-in and must not appear in the manifest"
)

def test_includes_aliases_as_first_class_slashes(self):
"""Aliases (/btw, /bg, /reset, /q) must be registered as standalone
slashes β€” this is the whole point of native-slashes parity."""
Expand All @@ -319,6 +330,9 @@ def test_telegram_parity(self):
Telegram but not Slack (because of Slack's 50-slash cap), this
test fails loudly so we can curate the list rather than silently
dropping parity.

Slack-reserved built-in commands (e.g. /status) are excluded
from parity checks since they cannot be registered on Slack.
"""
slack_names = {n for n, _d, _h in slack_native_slashes()}
tg_names = {n for n, _d in telegram_bot_commands()}
Expand All @@ -329,7 +343,8 @@ def _norm(s: str) -> str:

slack_norm = {_norm(n) for n in slack_names}
tg_norm = {_norm(n) for n in tg_names}
missing = tg_norm - slack_norm
reserved_norm = {_norm(n) for n in _SLACK_RESERVED_COMMANDS}
missing = (tg_norm - slack_norm) - reserved_norm
assert not missing, (
f"commands on Telegram but missing from Slack native slashes: {sorted(missing)}"
)
Expand Down
Loading