Currently, retrieving DMs with a given username requires several actions: first get the current user's ID; list all users and find the ID of the username; then scan all DM conversations and find the one with the current user's ID and the username's ID, to finally retrieve the messages using that conversation ID. This tool abstracts all that in a single call. PS: we'll implement a similar tool for multi-person DM conversations in a subsequent PR.
820 lines
28 KiB
Python
820 lines
28 KiB
Python
import copy
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import Mock, call, patch
|
|
|
|
import pytest
|
|
from arcade.sdk.errors import RetryableToolError, ToolExecutionError
|
|
from slack_sdk.errors import SlackApiError
|
|
from slack_sdk.web.async_slack_response import AsyncSlackResponse
|
|
|
|
from arcade_slack.constants import MAX_PAGINATION_SIZE_LIMIT
|
|
from arcade_slack.models import ConversationType, ConversationTypeSlackName
|
|
from arcade_slack.tools.chat import (
|
|
get_channel_metadata_by_name,
|
|
get_conversation_metadata_by_id,
|
|
get_direct_message_conversation_metadata_by_username,
|
|
get_members_in_channel_by_name,
|
|
get_members_in_conversation_by_id,
|
|
get_messages_in_channel_by_name,
|
|
get_messages_in_conversation_by_id,
|
|
get_messages_in_direct_message_conversation_by_username,
|
|
list_conversations_metadata,
|
|
list_direct_message_conversations_metadata,
|
|
list_group_direct_message_conversations_metadata,
|
|
list_private_channels_metadata,
|
|
list_public_channels_metadata,
|
|
send_dm_to_user,
|
|
send_message_to_channel,
|
|
)
|
|
from arcade_slack.utils import extract_basic_user_info, extract_conversation_metadata
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_list_conversations_metadata(mocker):
|
|
return mocker.patch("arcade_slack.tools.chat.list_conversations_metadata", autospec=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_channel_info() -> dict:
|
|
return {"name": "general", "id": "C12345", "is_member": True, "is_channel": True}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_dm_to_user(mock_context, mock_chat_slack_client):
|
|
mock_chat_slack_client.users_list.return_value = {
|
|
"ok": True,
|
|
"members": [{"name": "testuser", "id": "U12345"}],
|
|
}
|
|
mock_chat_slack_client.conversations_open.return_value = {
|
|
"ok": True,
|
|
"channel": {"id": "D12345"},
|
|
}
|
|
mock_slack_response = Mock(spec=AsyncSlackResponse)
|
|
mock_slack_response.data = {"ok": True}
|
|
mock_chat_slack_client.chat_postMessage.return_value = mock_slack_response
|
|
|
|
response = await send_dm_to_user(mock_context, "testuser", "Hello!")
|
|
|
|
assert response["response"]["ok"] is True
|
|
mock_chat_slack_client.users_list.assert_called_once()
|
|
mock_chat_slack_client.conversations_open.assert_called_once_with(users=["U12345"])
|
|
mock_chat_slack_client.chat_postMessage.assert_called_once_with(channel="D12345", text="Hello!")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_dm_to_inexistent_user(mock_context, mock_chat_slack_client):
|
|
mock_chat_slack_client.users_list.return_value = {
|
|
"ok": True,
|
|
"members": [{"name": "testuser", "id": "U12345"}],
|
|
}
|
|
|
|
with pytest.raises(RetryableToolError):
|
|
await send_dm_to_user(mock_context, "inexistent_user", "Hello!")
|
|
|
|
mock_chat_slack_client.users_list.assert_called_once()
|
|
mock_chat_slack_client.conversations_open.assert_not_called()
|
|
mock_chat_slack_client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_to_channel(mock_context, mock_chat_slack_client):
|
|
mock_chat_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [{"id": "C12345", "name": "general"}],
|
|
}
|
|
mock_slack_response = Mock(spec=AsyncSlackResponse)
|
|
mock_slack_response.data = {"ok": True}
|
|
mock_chat_slack_client.chat_postMessage.return_value = mock_slack_response
|
|
|
|
response = await send_message_to_channel(mock_context, "general", "Hello, channel!")
|
|
|
|
assert response["response"]["ok"] is True
|
|
mock_chat_slack_client.conversations_list.assert_called_once()
|
|
mock_chat_slack_client.chat_postMessage.assert_called_once_with(
|
|
channel="C12345", text="Hello, channel!"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_to_inexistent_channel(mock_context, mock_chat_slack_client):
|
|
mock_chat_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [],
|
|
}
|
|
|
|
with pytest.raises(RetryableToolError):
|
|
await send_message_to_channel(mock_context, "inexistent_channel", "Hello!")
|
|
|
|
mock_chat_slack_client.conversations_list.assert_called_once()
|
|
mock_chat_slack_client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_conversations_metadata_with_default_args(
|
|
mock_context, mock_chat_slack_client, mock_channel_info
|
|
):
|
|
mock_chat_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [mock_channel_info],
|
|
}
|
|
|
|
response = await list_conversations_metadata(mock_context)
|
|
|
|
assert response["conversations"] == [extract_conversation_metadata(mock_channel_info)]
|
|
assert response["next_cursor"] is None
|
|
|
|
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
|
types=",".join([conv_type.value for conv_type in ConversationTypeSlackName]),
|
|
exclude_archived=True,
|
|
limit=MAX_PAGINATION_SIZE_LIMIT,
|
|
cursor=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_conversations_metadata_filtering_single_conversation_type(
|
|
mock_context, mock_chat_slack_client, mock_channel_info
|
|
):
|
|
mock_chat_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [mock_channel_info],
|
|
}
|
|
|
|
response = await list_conversations_metadata(
|
|
mock_context, conversation_types=ConversationType.PUBLIC_CHANNEL
|
|
)
|
|
|
|
assert response["conversations"] == [extract_conversation_metadata(mock_channel_info)]
|
|
assert response["next_cursor"] is None
|
|
|
|
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
|
types=ConversationTypeSlackName.PUBLIC_CHANNEL.value,
|
|
exclude_archived=True,
|
|
limit=MAX_PAGINATION_SIZE_LIMIT,
|
|
cursor=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_conversations_metadata_filtering_multiple_conversation_types(
|
|
mock_context, mock_chat_slack_client, mock_channel_info
|
|
):
|
|
mock_chat_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [mock_channel_info],
|
|
}
|
|
|
|
response = await list_conversations_metadata(
|
|
mock_context,
|
|
conversation_types=[
|
|
ConversationTypeSlackName.PUBLIC_CHANNEL,
|
|
ConversationTypeSlackName.PRIVATE_CHANNEL,
|
|
],
|
|
)
|
|
|
|
assert response["conversations"] == [extract_conversation_metadata(mock_channel_info)]
|
|
assert response["next_cursor"] is None
|
|
|
|
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
|
types=f"{ConversationTypeSlackName.PUBLIC_CHANNEL.value},{ConversationTypeSlackName.PRIVATE_CHANNEL.value}",
|
|
exclude_archived=True,
|
|
limit=MAX_PAGINATION_SIZE_LIMIT,
|
|
cursor=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_conversations_metadata_with_custom_pagination_args(
|
|
mock_context, mock_chat_slack_client, mock_channel_info
|
|
):
|
|
mock_chat_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [mock_channel_info] * 3,
|
|
"response_metadata": {"next_cursor": "456"},
|
|
}
|
|
|
|
response = await list_conversations_metadata(mock_context, limit=3, next_cursor="123")
|
|
|
|
assert response["conversations"] == [
|
|
extract_conversation_metadata(mock_channel_info) for _ in range(3)
|
|
]
|
|
assert response["next_cursor"] == "456"
|
|
|
|
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
|
types=",".join([conv_type.value for conv_type in ConversationTypeSlackName]),
|
|
exclude_archived=True,
|
|
limit=3,
|
|
cursor="123",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"faulty_slack_function_name, tool_function, tool_args",
|
|
[
|
|
("users_list", send_dm_to_user, ("testuser", "Hello!")),
|
|
("conversations_list", send_message_to_channel, ("general", "Hello!")),
|
|
],
|
|
)
|
|
async def test_tools_with_slack_error(
|
|
mock_context, mock_chat_slack_client, faulty_slack_function_name, tool_function, tool_args
|
|
):
|
|
getattr(mock_chat_slack_client, faulty_slack_function_name).side_effect = SlackApiError(
|
|
message="test_slack_error",
|
|
response={"ok": False, "error": "test_slack_error"},
|
|
)
|
|
|
|
with pytest.raises(ToolExecutionError) as e:
|
|
await tool_function(mock_context, *tool_args)
|
|
assert "test_slack_error" in str(e.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"tool_function, conversation_type",
|
|
[
|
|
(list_public_channels_metadata, ConversationType.PUBLIC_CHANNEL),
|
|
(list_private_channels_metadata, ConversationType.PRIVATE_CHANNEL),
|
|
(
|
|
list_group_direct_message_conversations_metadata,
|
|
ConversationType.MULTI_PERSON_DIRECT_MESSAGE,
|
|
),
|
|
(list_direct_message_conversations_metadata, ConversationType.DIRECT_MESSAGE),
|
|
],
|
|
)
|
|
async def test_list_channels_metadata(
|
|
mock_context,
|
|
mock_list_conversations_metadata,
|
|
tool_function,
|
|
conversation_type,
|
|
):
|
|
response = await tool_function(mock_context, limit=3)
|
|
|
|
mock_list_conversations_metadata.assert_called_once_with(
|
|
mock_context, conversation_types=[conversation_type], limit=3
|
|
)
|
|
|
|
assert response == mock_list_conversations_metadata.return_value
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_conversation_metadata_by_id(
|
|
mock_context, mock_chat_slack_client, mock_channel_info
|
|
):
|
|
mock_chat_slack_client.conversations_info.return_value = {
|
|
"ok": True,
|
|
"channel": mock_channel_info,
|
|
}
|
|
|
|
response = await get_conversation_metadata_by_id(mock_context, "C12345")
|
|
|
|
assert response == extract_conversation_metadata(mock_channel_info)
|
|
mock_chat_slack_client.conversations_info.assert_called_once_with(
|
|
channel="C12345",
|
|
include_locale=True,
|
|
include_num_members=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.list_conversations_metadata")
|
|
async def test_get_conversation_metadata_by_id_slack_api_error(
|
|
mock_list_conversations_metadata, mock_context, mock_chat_slack_client, mock_channel_info
|
|
):
|
|
mock_channel_info["name"] = "whatever_conversation_should_be_present_in_additional_prompt"
|
|
mock_list_conversations_metadata.return_value = {
|
|
"conversations": [extract_conversation_metadata(mock_channel_info)],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
mock_chat_slack_client.conversations_info.side_effect = SlackApiError(
|
|
message="channel_not_found",
|
|
response={"ok": False, "error": "channel_not_found"},
|
|
)
|
|
|
|
with pytest.raises(RetryableToolError) as e:
|
|
await get_conversation_metadata_by_id(mock_context, "C12345")
|
|
|
|
assert (
|
|
"whatever_conversation_should_be_present_in_additional_prompt"
|
|
in e.additional_prompt_content
|
|
)
|
|
|
|
mock_chat_slack_client.conversations_info.assert_called_once_with(
|
|
channel="C12345",
|
|
include_locale=True,
|
|
include_num_members=True,
|
|
)
|
|
mock_list_conversations_metadata.assert_called_once_with(mock_context)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_conversation_metadata_by_name(
|
|
mock_context, mock_list_conversations_metadata, mock_channel_info
|
|
):
|
|
sample_conversation = extract_conversation_metadata(mock_channel_info)
|
|
mock_list_conversations_metadata.return_value = {
|
|
"conversations": [sample_conversation],
|
|
"next_cursor": None,
|
|
}
|
|
|
|
response = await get_channel_metadata_by_name(mock_context, sample_conversation["name"])
|
|
|
|
assert response == sample_conversation
|
|
mock_list_conversations_metadata.assert_called_once_with(mock_context, next_cursor=None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_metadata_by_name_triggering_pagination(
|
|
mock_context, mock_list_conversations_metadata, mock_channel_info
|
|
):
|
|
target_channel = extract_conversation_metadata(mock_channel_info)
|
|
another_channel = extract_conversation_metadata(mock_channel_info)
|
|
another_channel["name"] = "another_channel"
|
|
|
|
mock_list_conversations_metadata.side_effect = [
|
|
{
|
|
"conversations": [another_channel],
|
|
"next_cursor": "123",
|
|
},
|
|
{
|
|
"conversations": [target_channel],
|
|
"next_cursor": None,
|
|
},
|
|
]
|
|
|
|
response = await get_channel_metadata_by_name(mock_context, target_channel["name"])
|
|
|
|
assert response == target_channel
|
|
assert mock_list_conversations_metadata.call_count == 2
|
|
mock_list_conversations_metadata.assert_has_calls([
|
|
call(mock_context, next_cursor=None),
|
|
call(mock_context, next_cursor="123"),
|
|
])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_metadata_by_name_not_found(
|
|
mock_context, mock_list_conversations_metadata, mock_channel_info
|
|
):
|
|
first_channel = extract_conversation_metadata(mock_channel_info)
|
|
second_channel = extract_conversation_metadata(mock_channel_info)
|
|
second_channel["name"] = "second_channel"
|
|
|
|
mock_list_conversations_metadata.side_effect = [
|
|
{
|
|
"conversations": [second_channel],
|
|
"next_cursor": "123",
|
|
},
|
|
{
|
|
"conversations": [first_channel],
|
|
"next_cursor": None,
|
|
},
|
|
]
|
|
|
|
with pytest.raises(RetryableToolError):
|
|
await get_channel_metadata_by_name(mock_context, "inexistent_channel")
|
|
|
|
assert mock_list_conversations_metadata.call_count == 2
|
|
mock_list_conversations_metadata.assert_has_calls([
|
|
call(mock_context, next_cursor=None),
|
|
call(mock_context, next_cursor="123"),
|
|
])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.async_paginate")
|
|
@patch("arcade_slack.tools.chat.get_user_info_by_id")
|
|
async def test_get_members_from_conversation_id(
|
|
mock_get_user_info_by_id, mock_async_paginate, mock_context, mock_chat_slack_client
|
|
):
|
|
member1 = {"id": "U123", "name": "testuser123"}
|
|
member1_info = extract_basic_user_info(member1)
|
|
member2 = {"id": "U456", "name": "testuser456"}
|
|
member2_info = extract_basic_user_info(member2)
|
|
|
|
mock_async_paginate.return_value = [member1["id"], member2["id"]], "token123"
|
|
mock_get_user_info_by_id.side_effect = [member1_info, member2_info]
|
|
|
|
response = await get_members_in_conversation_by_id(
|
|
mock_context, conversation_id="C12345", limit=2
|
|
)
|
|
|
|
assert response == {
|
|
"members": [member1_info, member2_info],
|
|
"next_cursor": "token123",
|
|
}
|
|
mock_async_paginate.assert_called_once_with(
|
|
mock_chat_slack_client.conversations_members,
|
|
"members",
|
|
limit=2,
|
|
next_cursor=None,
|
|
channel="C12345",
|
|
)
|
|
mock_get_user_info_by_id.assert_has_calls([
|
|
call(mock_context, member1["id"]),
|
|
call(mock_context, member2["id"]),
|
|
])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.async_paginate")
|
|
@patch("arcade_slack.tools.chat.get_user_info_by_id")
|
|
@patch("arcade_slack.tools.chat.list_conversations_metadata")
|
|
async def test_get_members_from_conversation_id_channel_not_found(
|
|
mock_list_conversations_metadata,
|
|
mock_get_user_info_by_id,
|
|
mock_async_paginate,
|
|
mock_context,
|
|
mock_chat_slack_client,
|
|
mock_channel_info,
|
|
):
|
|
conversations = [extract_conversation_metadata(mock_channel_info)] * 2
|
|
mock_list_conversations_metadata.return_value = {
|
|
"conversations": conversations,
|
|
"next_cursor": None,
|
|
}
|
|
|
|
member1 = {"id": "U123", "name": "testuser123"}
|
|
member1_info = extract_basic_user_info(member1)
|
|
member2 = {"id": "U456", "name": "testuser456"}
|
|
member2_info = extract_basic_user_info(member2)
|
|
|
|
mock_async_paginate.side_effect = SlackApiError(
|
|
message="channel_not_found",
|
|
response={"ok": False, "error": "channel_not_found"},
|
|
)
|
|
mock_get_user_info_by_id.side_effect = [member1_info, member2_info]
|
|
|
|
with pytest.raises(RetryableToolError):
|
|
await get_members_in_conversation_by_id(mock_context, conversation_id="C12345", limit=2)
|
|
|
|
mock_async_paginate.assert_called_once_with(
|
|
mock_chat_slack_client.conversations_members,
|
|
"members",
|
|
limit=2,
|
|
next_cursor=None,
|
|
channel="C12345",
|
|
)
|
|
mock_get_user_info_by_id.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.list_conversations_metadata")
|
|
@patch("arcade_slack.tools.chat.get_members_in_conversation_by_id")
|
|
async def test_get_members_in_channel_by_name(
|
|
mock_get_members_in_conversation_by_id,
|
|
mock_list_conversations_metadata,
|
|
mock_context,
|
|
mock_channel_info,
|
|
):
|
|
mock_list_conversations_metadata.return_value = {
|
|
"conversations": [extract_conversation_metadata(mock_channel_info)],
|
|
"next_cursor": None,
|
|
}
|
|
|
|
response = await get_members_in_channel_by_name(
|
|
mock_context, mock_channel_info["name"], limit=2
|
|
)
|
|
|
|
assert response == mock_get_members_in_conversation_by_id.return_value
|
|
mock_list_conversations_metadata.assert_called_once_with(mock_context, next_cursor=None)
|
|
mock_get_members_in_conversation_by_id.assert_called_once_with(
|
|
context=mock_context,
|
|
conversation_id="C12345",
|
|
limit=2,
|
|
next_cursor=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.list_conversations_metadata")
|
|
@patch("arcade_slack.tools.chat.get_members_in_conversation_by_id")
|
|
async def test_get_members_in_channel_by_name_triggering_pagination(
|
|
mock_get_members_in_conversation_by_id,
|
|
mock_list_conversations_metadata,
|
|
mock_context,
|
|
mock_channel_info,
|
|
):
|
|
conversation1 = copy.deepcopy(mock_channel_info)
|
|
conversation1["name"] = "conversation1"
|
|
conversation2 = copy.deepcopy(mock_channel_info)
|
|
conversation2["name"] = "conversation2"
|
|
|
|
mock_list_conversations_metadata.side_effect = [
|
|
{
|
|
"conversations": [extract_conversation_metadata(conversation1)],
|
|
"next_cursor": "123",
|
|
},
|
|
{
|
|
"conversations": [extract_conversation_metadata(conversation2)],
|
|
"next_cursor": None,
|
|
},
|
|
]
|
|
|
|
response = await get_members_in_channel_by_name(mock_context, conversation2["name"], limit=2)
|
|
|
|
assert response == mock_get_members_in_conversation_by_id.return_value
|
|
mock_list_conversations_metadata.assert_has_calls([
|
|
call(mock_context, next_cursor=None),
|
|
call(mock_context, next_cursor="123"),
|
|
])
|
|
mock_get_members_in_conversation_by_id.assert_called_once_with(
|
|
context=mock_context,
|
|
conversation_id="C12345",
|
|
limit=2,
|
|
next_cursor=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_conversation_history_by_id(mock_context, mock_chat_slack_client):
|
|
mock_chat_slack_client.conversations_history.return_value = {
|
|
"ok": True,
|
|
"messages": [{"text": "Hello, world!"}],
|
|
}
|
|
|
|
response = await get_messages_in_conversation_by_id(mock_context, "C12345", limit=1)
|
|
|
|
assert response == {"messages": [{"text": "Hello, world!"}], "next_cursor": None}
|
|
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
|
channel="C12345",
|
|
include_all_metadata=True,
|
|
inclusive=True,
|
|
limit=1,
|
|
cursor=None,
|
|
)
|
|
|
|
|
|
# TODO: pass a current unix timestamp to the tool, instead of mocking the datetime
|
|
# conversion. Have to wait until arcade.core.annotations.Inferrable is implemented.
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.convert_relative_datetime_to_unix_timestamp")
|
|
@patch("arcade_slack.tools.chat.datetime")
|
|
async def test_get_conversation_history_by_id_with_relative_datetime_args(
|
|
mock_datetime,
|
|
mock_convert_relative_datetime_to_unix_timestamp,
|
|
mock_context,
|
|
mock_chat_slack_client,
|
|
):
|
|
mock_chat_slack_client.conversations_history.return_value = {
|
|
"ok": True,
|
|
"messages": [{"text": "Hello, world!"}],
|
|
}
|
|
|
|
expected_oldest_timestamp = 1716489600
|
|
expected_latest_timestamp = 1716403200
|
|
|
|
# Ideally we'd pass the current unix timestamp to the function, instead of mocking, but
|
|
# currently there's no way to have a tool argument that is not exposed to the LLM. We
|
|
# should have that soon, though.
|
|
mock_datetime.now.return_value = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
|
expected_current_unix_timestamp = int(mock_datetime.now.return_value.timestamp())
|
|
mock_convert_relative_datetime_to_unix_timestamp.side_effect = [
|
|
expected_latest_timestamp,
|
|
expected_oldest_timestamp,
|
|
]
|
|
|
|
response = await get_messages_in_conversation_by_id(
|
|
mock_context, "C12345", oldest_relative="02:00:00", latest_relative="01:00:00", limit=1
|
|
)
|
|
|
|
assert response == {"messages": [{"text": "Hello, world!"}], "next_cursor": None}
|
|
mock_convert_relative_datetime_to_unix_timestamp.assert_has_calls([
|
|
call("01:00:00", expected_current_unix_timestamp),
|
|
call("02:00:00", expected_current_unix_timestamp),
|
|
])
|
|
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
|
channel="C12345",
|
|
include_all_metadata=True,
|
|
inclusive=True,
|
|
limit=1,
|
|
cursor=None,
|
|
oldest=expected_oldest_timestamp,
|
|
latest=expected_latest_timestamp,
|
|
)
|
|
|
|
|
|
# TODO: pass a current unix timestamp to the tool, instead of mocking the datetime
|
|
# conversion. Have to wait until arcade.core.annotations.Inferrable is implemented.
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.convert_datetime_to_unix_timestamp")
|
|
async def test_get_conversation_history_by_id_with_absolute_datetime_args(
|
|
mock_convert_datetime_to_unix_timestamp, mock_context, mock_chat_slack_client
|
|
):
|
|
mock_chat_slack_client.conversations_history.return_value = {
|
|
"ok": True,
|
|
"messages": [{"text": "Hello, world!"}],
|
|
}
|
|
|
|
expected_latest_timestamp = 1716403200
|
|
expected_oldest_timestamp = 1716489600
|
|
|
|
# Ideally we'd pass the current unix timestamp to the function, instead of mocking, but
|
|
# currently there's no way to have a tool argument that is not exposed to the LLM. We
|
|
# should have that soon, though.
|
|
mock_convert_datetime_to_unix_timestamp.side_effect = [
|
|
expected_latest_timestamp,
|
|
expected_oldest_timestamp,
|
|
]
|
|
|
|
response = await get_messages_in_conversation_by_id(
|
|
mock_context,
|
|
"C12345",
|
|
oldest_datetime="2025-01-01 00:00:00",
|
|
latest_datetime="2025-01-02 00:00:00",
|
|
limit=1,
|
|
)
|
|
|
|
assert response == {"messages": [{"text": "Hello, world!"}], "next_cursor": None}
|
|
mock_convert_datetime_to_unix_timestamp.assert_has_calls([
|
|
call("2025-01-02 00:00:00"),
|
|
call("2025-01-01 00:00:00"),
|
|
])
|
|
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
|
channel="C12345",
|
|
include_all_metadata=True,
|
|
inclusive=True,
|
|
limit=1,
|
|
cursor=None,
|
|
oldest=expected_oldest_timestamp,
|
|
latest=expected_latest_timestamp,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_conversation_history_by_id_with_messed_oldest_args(
|
|
mock_context, mock_chat_slack_client
|
|
):
|
|
with pytest.raises(ToolExecutionError):
|
|
await get_messages_in_conversation_by_id(
|
|
mock_context,
|
|
"C12345",
|
|
oldest_datetime="2025-01-01 00:00:00",
|
|
oldest_relative="01:00:00",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_conversation_history_by_id_with_messed_latest_args(
|
|
mock_context, mock_chat_slack_client
|
|
):
|
|
with pytest.raises(ToolExecutionError):
|
|
await get_messages_in_conversation_by_id(
|
|
mock_context,
|
|
"C12345",
|
|
latest_datetime="2025-01-01 00:00:00",
|
|
latest_relative="01:00:00",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_conversation_history_by_name(mock_context, mock_chat_slack_client):
|
|
mock_chat_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [
|
|
{
|
|
"id": "C12345",
|
|
"name": "general",
|
|
"is_member": True,
|
|
"is_channel": True,
|
|
}
|
|
],
|
|
}
|
|
mock_chat_slack_client.conversations_history.return_value = {
|
|
"ok": True,
|
|
"messages": [{"text": "Hello, world!"}],
|
|
}
|
|
|
|
response = await get_messages_in_channel_by_name(mock_context, "general", limit=1)
|
|
|
|
assert response == {"messages": [{"text": "Hello, world!"}], "next_cursor": None}
|
|
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
|
channel="C12345", include_all_metadata=True, inclusive=True, limit=1, cursor=None
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.retrieve_conversations_by_user_ids")
|
|
async def test_get_direct_message_conversation_metadata_by_username(
|
|
mock_retrieve_conversations_by_user_ids,
|
|
mock_context,
|
|
mock_chat_slack_client,
|
|
mock_users_slack_client,
|
|
):
|
|
mock_chat_slack_client.auth_test.return_value = {
|
|
"ok": True,
|
|
"user_id": "U1",
|
|
"team_id": "T1",
|
|
"user": "user1",
|
|
}
|
|
|
|
mock_users_slack_client.users_list.return_value = {
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "U1", "name": "user1"},
|
|
{"id": "U2", "name": "user2"},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
conversation = {
|
|
"id": "C12345",
|
|
"type": ConversationTypeSlackName.IM.value,
|
|
"is_im": True,
|
|
"members": ["U1", "U2"],
|
|
}
|
|
|
|
mock_retrieve_conversations_by_user_ids.return_value = [conversation]
|
|
|
|
response = await get_direct_message_conversation_metadata_by_username(
|
|
context=mock_context, username="user2"
|
|
)
|
|
|
|
assert response == conversation
|
|
mock_retrieve_conversations_by_user_ids.assert_called_once_with(
|
|
list_conversations_func=list_conversations_metadata,
|
|
get_members_in_conversation_func=get_members_in_conversation_by_id,
|
|
context=mock_context,
|
|
conversation_types=[ConversationType.DIRECT_MESSAGE],
|
|
user_ids=["U1", "U2"],
|
|
exact_match=True,
|
|
limit=1,
|
|
next_cursor=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.retrieve_conversations_by_user_ids")
|
|
async def test_get_direct_message_conversation_metadata_by_username_username_not_found(
|
|
mock_retrieve_conversations_by_user_ids,
|
|
mock_context,
|
|
mock_chat_slack_client,
|
|
mock_users_slack_client,
|
|
):
|
|
mock_chat_slack_client.users_identity.return_value = {
|
|
"ok": True,
|
|
"user": {"id": "U1", "name": "user1"},
|
|
"team": {"id": "T1", "name": "team1"},
|
|
}
|
|
|
|
mock_users_slack_client.users_list.return_value = {
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "U1", "name": "user1"},
|
|
{"id": "U2", "name": "user2"},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_retrieve_conversations_by_user_ids.side_effect = TimeoutError()
|
|
|
|
with pytest.raises(RetryableToolError):
|
|
await get_direct_message_conversation_metadata_by_username(
|
|
context=mock_context, username="user999"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.get_messages_in_conversation_by_id")
|
|
@patch("arcade_slack.tools.chat.get_direct_message_conversation_metadata_by_username")
|
|
async def test_get_messages_in_direct_conversation_by_username(
|
|
mock_get_direct_message_conversation_metadata_by_username,
|
|
mock_get_messages_in_conversation_by_id,
|
|
mock_context,
|
|
):
|
|
mock_get_direct_message_conversation_metadata_by_username.return_value = {
|
|
"id": "C12345",
|
|
}
|
|
|
|
response = await get_messages_in_direct_message_conversation_by_username(
|
|
context=mock_context, username="user2"
|
|
)
|
|
|
|
assert response == mock_get_messages_in_conversation_by_id.return_value
|
|
mock_get_direct_message_conversation_metadata_by_username.assert_called_once_with(
|
|
context=mock_context, username="user2"
|
|
)
|
|
mock_get_messages_in_conversation_by_id.assert_called_once_with(
|
|
context=mock_context,
|
|
conversation_id="C12345",
|
|
oldest_relative=None,
|
|
latest_relative=None,
|
|
oldest_datetime=None,
|
|
latest_datetime=None,
|
|
limit=None,
|
|
next_cursor=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("arcade_slack.tools.chat.get_direct_message_conversation_metadata_by_username")
|
|
async def test_get_messages_in_direct_conversation_by_username_not_found(
|
|
mock_get_direct_message_conversation_metadata_by_username,
|
|
mock_context,
|
|
):
|
|
mock_get_direct_message_conversation_metadata_by_username.return_value = None
|
|
|
|
with pytest.raises(ToolExecutionError):
|
|
await get_messages_in_direct_message_conversation_by_username(
|
|
context=mock_context, username="user2"
|
|
)
|