fix(slack): handle leading-space text commands

This commit is contained in:
nu 2026-07-02 10:03:19 +09:00 committed by Teknium
parent cbc1054e23
commit 3e05b39e4b
No known key found for this signature in database
3 changed files with 29 additions and 8 deletions

View file

@ -1826,14 +1826,15 @@ class MessageEvent:
def is_command(self) -> bool:
"""Check if this is a command message (e.g., /new, /reset)."""
return self.text.startswith("/")
return (self.text or "").lstrip().startswith("/")
def get_command(self) -> Optional[str]:
"""Extract command name if this is a command message."""
if not self.is_command():
return None
# Split on space and get first word, strip the /
parts = self.text.split(maxsplit=1)
command_text = (self.text or "").lstrip()
parts = command_text.split(maxsplit=1)
raw = parts[0][1:].lower() if parts else None
if raw and "@" in raw:
raw = raw.split("@", 1)[0]
@ -1846,7 +1847,8 @@ class MessageEvent:
"""Get the arguments after a command."""
if not self.is_command():
return self.text
parts = self.text.split(maxsplit=1)
command_text = (self.text or "").lstrip()
parts = command_text.split(maxsplit=1)
args = parts[1] if len(parts) > 1 else ""
# iOS auto-corrects -- to — (em dash) and - to (en dash)
args = args.replace("\u2014\u2014", "--").replace("\u2014", "--").replace("\u2013", "-")

View file

@ -3788,11 +3788,12 @@ class SlackAdapter(BasePlatformAdapter):
# gateway dispatcher) handles it like a normal slash command. Only
# rewrite when the first token resolves to a known gateway command
# so casual messages like "!nice work" pass through unchanged.
if original_text.startswith("!"):
command_probe_text = original_text.lstrip()
if command_probe_text.startswith("!"):
try:
from hermes_cli.commands import is_gateway_known_command
first_token = original_text[1:].split(maxsplit=1)[0]
first_token = command_probe_text[1:].split(maxsplit=1)[0]
# Strip "@suffix" the same way get_command() does, so
# forms like ``!stop@hermes`` still resolve.
cmd_name = first_token.split("@", 1)[0].lower()
@ -3801,10 +3802,12 @@ class SlackAdapter(BasePlatformAdapter):
and "/" not in cmd_name
and is_gateway_known_command(cmd_name)
):
original_text = "/" + original_text[1:]
original_text = "/" + command_probe_text[1:]
command_probe_text = original_text
except Exception: # pragma: no cover - defensive
pass
is_command_text = command_probe_text.startswith("/")
text = original_text
# Extract quoted/forwarded content from Slack blocks.
@ -4163,9 +4166,15 @@ class SlackAdapter(BasePlatformAdapter):
# Determine message type
msg_type = MessageType.TEXT
if (original_text or "").startswith("/"):
if is_command_text:
msg_type = MessageType.COMMAND
# Commands typed as Slack text messages often intentionally carry a
# leading space (`` /stop``) so Slack itself does not intercept the
# slash. Once classified as a command, pass only the command text into
# the gateway dispatcher; do not prepend fetched thread context or
# block/attachment rendering before the leading slash.
# Handle file attachments
media_urls = []
media_types = []
@ -4514,7 +4523,7 @@ class SlackAdapter(BasePlatformAdapter):
)
msg_event = MessageEvent(
text=text,
text=(command_probe_text if is_command_text else text),
message_type=msg_type,
source=source,
raw_message=event,

View file

@ -56,6 +56,16 @@ class TestSlashCommands:
response_lower = response_text.lower()
assert "no" in response_lower or "stop" in response_lower or "not running" in response_lower
@pytest.mark.asyncio
async def test_leading_space_stop_is_still_a_command(self, adapter, platform):
"""Slack users type `` /stop`` to avoid native Slack slash interception."""
send = await send_and_capture(adapter, " /stop", platform)
send.assert_called_once()
response_text = send.call_args[1].get("content") or send.call_args[0][1]
response_lower = response_text.lower()
assert "no" in response_lower or "stop" in response_lower or "not running" in response_lower
@pytest.mark.asyncio
async def test_commands_shows_listing(self, adapter, platform):
send = await send_and_capture(adapter, "/commands", platform)