mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(slack): open DMs for user send targets
This commit is contained in:
parent
99cafc8eac
commit
3997b0907e
2 changed files with 250 additions and 41 deletions
|
|
@ -29,6 +29,7 @@ from gateway.config import Platform
|
|||
from tools.send_message_tool import (
|
||||
_is_telegram_thread_not_found,
|
||||
_parse_target_ref,
|
||||
_resolve_slack_user_target,
|
||||
_send_matrix_via_adapter,
|
||||
_send_signal,
|
||||
_send_telegram,
|
||||
|
|
@ -1620,7 +1621,7 @@ class TestParseTargetRefWhatsAppJID:
|
|||
|
||||
|
||||
class TestParseTargetRefSlack:
|
||||
"""_parse_target_ref recognizes Slack channel/user IDs as explicit."""
|
||||
"""_parse_target_ref recognizes Slack conversation and user targets."""
|
||||
|
||||
def test_thread_target_is_explicit(self):
|
||||
chat_id, thread_id, is_explicit = _parse_target_ref("slack", "C0B0QV5434G:171.000001")
|
||||
|
|
@ -1640,12 +1641,22 @@ class TestParseTargetRefSlack:
|
|||
def test_dm_id_is_explicit(self):
|
||||
assert _parse_target_ref("slack", "D123ABCDEF")[2] is True
|
||||
|
||||
def test_user_id_is_not_explicit(self):
|
||||
"""Slack user IDs (U...) and workspace IDs (W...) are NOT explicit send
|
||||
targets. chat.postMessage rejects them — a DM must be opened first via
|
||||
conversations.open to obtain a D... conversation ID.
|
||||
"""
|
||||
assert _parse_target_ref("slack", "U123ABCDEF")[2] is False
|
||||
def test_user_id_is_explicit_dm_target(self):
|
||||
"""Slack user IDs are explicit user targets that must be opened as DMs."""
|
||||
chat_id, thread_id, is_explicit = _parse_target_ref("slack", "U123ABCDEF")
|
||||
assert chat_id == "user:U123ABCDEF"
|
||||
assert thread_id is None
|
||||
assert is_explicit is True
|
||||
|
||||
def test_at_username_is_explicit_dm_target(self):
|
||||
"""slack:@username targets resolve to DMs through users.list + conversations.open."""
|
||||
chat_id, thread_id, is_explicit = _parse_target_ref("slack", "@alice")
|
||||
assert chat_id == "user_name:alice"
|
||||
assert thread_id is None
|
||||
assert is_explicit is True
|
||||
|
||||
def test_workspace_id_is_not_explicit(self):
|
||||
"""Slack workspace IDs (W...) are not sendable conversation/user targets."""
|
||||
assert _parse_target_ref("slack", "W123ABCDEF")[2] is False
|
||||
|
||||
def test_whitespace_is_stripped(self):
|
||||
|
|
@ -1747,6 +1758,129 @@ class TestEmailHomeChannelErrorHint:
|
|||
assert "TELEGRAM_HOME_CHANNEL" in result["error"]
|
||||
|
||||
|
||||
class TestResolveSlackUserTargets:
|
||||
"""_resolve_slack_user_target opens user targets as DMs before sending.
|
||||
|
||||
Adapted from #19237's ``_send_slack`` tests: main moved Slack delivery to
|
||||
the plugin's ``_standalone_send`` (#41112), so the salvaged DM-open logic
|
||||
lives in a resolution helper that runs before any send path.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _mock_response(data):
|
||||
response = MagicMock()
|
||||
response.json = AsyncMock(return_value=data)
|
||||
response.__aenter__ = AsyncMock(return_value=response)
|
||||
response.__aexit__ = AsyncMock(return_value=None)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _mock_session(*responses):
|
||||
session = MagicMock()
|
||||
session.__aenter__ = AsyncMock(return_value=session)
|
||||
session.__aexit__ = AsyncMock(return_value=None)
|
||||
session.post = MagicMock(side_effect=responses)
|
||||
return session
|
||||
|
||||
def test_conversation_ids_pass_through_without_api_calls(self):
|
||||
for cid in ("C0B0QV5434G", "G123ABCDEF", "D123ABCDEF"):
|
||||
chat_id, err = asyncio.run(_resolve_slack_user_target("tok", cid))
|
||||
assert chat_id == cid
|
||||
assert err is None
|
||||
|
||||
def test_user_id_target_opens_dm(self):
|
||||
session = self._mock_session(
|
||||
self._mock_response({"ok": True, "channel": {"id": "D123ABCDEF"}}),
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession", return_value=session):
|
||||
chat_id, err = asyncio.run(
|
||||
_resolve_slack_user_target("tok", "user:U123ABCDEF")
|
||||
)
|
||||
|
||||
assert err is None
|
||||
assert chat_id == "D123ABCDEF"
|
||||
open_payload = session.post.call_args_list[0].kwargs["json"]
|
||||
assert open_payload == {"users": "U123ABCDEF"}
|
||||
|
||||
def test_username_target_resolves_user_then_opens_dm(self):
|
||||
session = self._mock_session(
|
||||
self._mock_response({
|
||||
"ok": True,
|
||||
"members": [
|
||||
{"id": "UOTHER123", "name": "someone", "profile": {"display_name": "Other", "real_name": "Other User"}},
|
||||
{"id": "U123ABCDEF", "name": "alice", "profile": {"display_name": "Alice", "real_name": "Alice Example"}},
|
||||
],
|
||||
"response_metadata": {},
|
||||
}),
|
||||
self._mock_response({"ok": True, "channel": {"id": "D123ABCDEF"}}),
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession", return_value=session):
|
||||
chat_id, err = asyncio.run(
|
||||
_resolve_slack_user_target("tok", "user_name:alice")
|
||||
)
|
||||
|
||||
assert err is None
|
||||
assert chat_id == "D123ABCDEF"
|
||||
assert session.post.call_args_list[1].kwargs["json"] == {"users": "U123ABCDEF"}
|
||||
|
||||
def test_username_target_does_not_match_display_or_real_name(self):
|
||||
session = self._mock_session(
|
||||
self._mock_response({
|
||||
"ok": True,
|
||||
"members": [
|
||||
{"id": "U123ABCDEF", "name": "notalice", "profile": {"display_name": "alice", "real_name": "alice"}},
|
||||
],
|
||||
"response_metadata": {},
|
||||
}),
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession", return_value=session):
|
||||
chat_id, err = asyncio.run(
|
||||
_resolve_slack_user_target("tok", "user_name:alice")
|
||||
)
|
||||
|
||||
assert chat_id is None
|
||||
assert "Could not resolve Slack user '@alice'" in err["error"]
|
||||
assert session.post.call_count == 1
|
||||
|
||||
def test_ambiguous_username_returns_error_without_opening_dm(self):
|
||||
session = self._mock_session(
|
||||
self._mock_response({
|
||||
"ok": True,
|
||||
"members": [
|
||||
{"id": "U111AAAAA", "name": "alice", "profile": {}},
|
||||
{"id": "U222BBBBB", "name": "alice", "profile": {}},
|
||||
],
|
||||
"response_metadata": {},
|
||||
}),
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession", return_value=session):
|
||||
chat_id, err = asyncio.run(
|
||||
_resolve_slack_user_target("tok", "user_name:alice")
|
||||
)
|
||||
|
||||
assert chat_id is None
|
||||
assert "matched multiple Slack users" in err["error"]
|
||||
assert session.post.call_count == 1
|
||||
|
||||
def test_conversations_open_failure_surfaces_error(self):
|
||||
session = self._mock_session(
|
||||
self._mock_response({"ok": False, "error": "missing_scope"}),
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession", return_value=session):
|
||||
chat_id, err = asyncio.run(
|
||||
_resolve_slack_user_target("tok", "user:U123ABCDEF")
|
||||
)
|
||||
|
||||
assert chat_id is None
|
||||
assert "missing_scope" in err["error"]
|
||||
assert "im:write" in err["error"]
|
||||
|
||||
|
||||
class TestSendDiscordThreadId:
|
||||
"""_send_discord uses thread_id when provided."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue