mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
462 lines
17 KiB
Python
462 lines
17 KiB
Python
"""
|
|
Tests for the SlackCallbackProcessor.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from integrations.models import Message
|
|
from server.conversation_callback_processor.slack_callback_processor import (
|
|
SlackCallbackProcessor,
|
|
)
|
|
from storage.conversation_callback import ConversationCallback
|
|
|
|
from openhands.core.schema.agent import AgentState
|
|
from openhands.events.action import MessageAction
|
|
from openhands.events.observation.agent import AgentStateChangedObservation
|
|
from openhands.server.shared import conversation_manager
|
|
|
|
|
|
@pytest.fixture
|
|
def slack_callback_processor():
|
|
"""Create a SlackCallbackProcessor instance for testing."""
|
|
return SlackCallbackProcessor(
|
|
slack_user_id='test_slack_user_id',
|
|
channel_id='test_channel_id',
|
|
message_ts='test_message_ts',
|
|
thread_ts='test_thread_ts',
|
|
team_id='test_team_id',
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def agent_state_changed_observation():
|
|
"""Create an AgentStateChangedObservation for testing."""
|
|
return AgentStateChangedObservation('', AgentState.AWAITING_USER_INPUT)
|
|
|
|
|
|
@pytest.fixture
|
|
def conversation_callback():
|
|
"""Create a ConversationCallback for testing."""
|
|
callback = MagicMock(spec=ConversationCallback)
|
|
return callback
|
|
|
|
|
|
class TestSlackCallbackProcessor:
|
|
"""Test the SlackCallbackProcessor class."""
|
|
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_summary_instruction'
|
|
)
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.conversation_manager'
|
|
)
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_last_user_msg_from_conversation_manager'
|
|
)
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.event_to_dict'
|
|
)
|
|
async def test_call_with_send_summary_instruction(
|
|
self,
|
|
mock_event_to_dict,
|
|
mock_get_last_user_msg,
|
|
mock_conversation_manager,
|
|
mock_get_summary_instruction,
|
|
slack_callback_processor,
|
|
agent_state_changed_observation,
|
|
conversation_callback,
|
|
):
|
|
"""Test the __call__ method when send_summary_instruction is True."""
|
|
# Setup mocks
|
|
mock_get_summary_instruction.return_value = (
|
|
'Please summarize this conversation.'
|
|
)
|
|
mock_msg = MagicMock()
|
|
mock_msg.id = 126
|
|
mock_msg.content = 'Hello'
|
|
mock_get_last_user_msg.return_value = [mock_msg] # Mock message with ID
|
|
mock_conversation_manager.send_event_to_conversation = AsyncMock()
|
|
mock_event_to_dict.return_value = {
|
|
'type': 'message_action',
|
|
'content': 'Please summarize this conversation.',
|
|
}
|
|
|
|
# Call the method
|
|
await slack_callback_processor(
|
|
callback=conversation_callback,
|
|
observation=agent_state_changed_observation,
|
|
)
|
|
|
|
# Verify the behavior
|
|
mock_get_summary_instruction.assert_called_once()
|
|
mock_event_to_dict.assert_called_once()
|
|
assert isinstance(mock_event_to_dict.call_args[0][0], MessageAction)
|
|
mock_conversation_manager.send_event_to_conversation.assert_called_once_with(
|
|
conversation_callback.conversation_id, mock_event_to_dict.return_value
|
|
)
|
|
|
|
# Verify the last_user_msg_id was updated
|
|
assert slack_callback_processor.last_user_msg_id == 126
|
|
|
|
# Verify the callback was updated and saved
|
|
conversation_callback.set_processor.assert_called_once_with(
|
|
slack_callback_processor
|
|
)
|
|
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.extract_summary_from_conversation_manager'
|
|
)
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_last_user_msg_from_conversation_manager'
|
|
)
|
|
@patch('server.conversation_callback_processor.slack_callback_processor.asyncio')
|
|
async def test_call_with_extract_summary(
|
|
self,
|
|
mock_asyncio,
|
|
mock_get_last_user_msg,
|
|
mock_extract_summary,
|
|
slack_callback_processor,
|
|
agent_state_changed_observation,
|
|
conversation_callback,
|
|
):
|
|
"""Test the __call__ method when last message is summary instruction."""
|
|
# Setup - simulate that last message was the summary instruction
|
|
mock_last_msg = MagicMock()
|
|
mock_last_msg.id = 127
|
|
mock_last_msg.content = 'Please summarize this conversation.'
|
|
mock_get_last_user_msg.return_value = [mock_last_msg]
|
|
mock_extract_summary.return_value = 'This is a summary of the conversation.'
|
|
|
|
# Mock get_summary_instruction to return the same content
|
|
with patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_summary_instruction',
|
|
return_value='Please summarize this conversation.',
|
|
):
|
|
# Call the method
|
|
await slack_callback_processor(
|
|
callback=conversation_callback,
|
|
observation=agent_state_changed_observation,
|
|
)
|
|
|
|
# Verify the behavior
|
|
mock_extract_summary.assert_called_once_with(
|
|
conversation_manager, conversation_callback.conversation_id
|
|
)
|
|
mock_asyncio.create_task.assert_called_once()
|
|
|
|
# Verify the last_user_msg_id was updated
|
|
assert slack_callback_processor.last_user_msg_id == 127
|
|
|
|
# Verify the callback was updated and saved
|
|
conversation_callback.set_processor.assert_called_once_with(
|
|
slack_callback_processor
|
|
)
|
|
|
|
async def test_call_with_error_agent_state(
|
|
self, slack_callback_processor, conversation_callback
|
|
):
|
|
"""Test the __call__ method when agent state is ERROR."""
|
|
# Create an observation with ERROR state
|
|
observation = AgentStateChangedObservation(
|
|
content='', agent_state=AgentState.ERROR, reason=''
|
|
)
|
|
|
|
# Call the method
|
|
await slack_callback_processor(
|
|
callback=conversation_callback, observation=observation
|
|
)
|
|
|
|
# Verify that nothing happens when agent state is ERROR (method returns early)
|
|
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.extract_summary_from_conversation_manager'
|
|
)
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_last_user_msg_from_conversation_manager'
|
|
)
|
|
@patch('server.conversation_callback_processor.slack_callback_processor.asyncio')
|
|
async def test_call_with_completed_agent_state(
|
|
self,
|
|
mock_asyncio,
|
|
mock_get_last_user_msg,
|
|
mock_extract_summary,
|
|
slack_callback_processor,
|
|
conversation_callback,
|
|
):
|
|
"""Test the __call__ method when agent state is COMPLETED."""
|
|
# Setup - simulate that last message was the summary instruction
|
|
mock_last_msg = MagicMock()
|
|
mock_last_msg.id = 124
|
|
mock_last_msg.content = 'Please summarize this conversation.'
|
|
mock_get_last_user_msg.return_value = [mock_last_msg]
|
|
mock_extract_summary.return_value = (
|
|
'This is a summary of the completed conversation.'
|
|
)
|
|
|
|
# Create an observation with FINISHED state (COMPLETED doesn't exist)
|
|
observation = AgentStateChangedObservation(
|
|
content='', agent_state=AgentState.FINISHED, reason=''
|
|
)
|
|
|
|
# Mock get_summary_instruction to return the same content
|
|
with patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_summary_instruction',
|
|
return_value='Please summarize this conversation.',
|
|
):
|
|
# Call the method
|
|
await slack_callback_processor(
|
|
callback=conversation_callback, observation=observation
|
|
)
|
|
|
|
# Verify the behavior
|
|
mock_extract_summary.assert_called_once_with(
|
|
conversation_manager, conversation_callback.conversation_id
|
|
)
|
|
mock_asyncio.create_task.assert_called_once()
|
|
|
|
# Verify the last_user_msg_id was updated
|
|
assert slack_callback_processor.last_user_msg_id == 124
|
|
|
|
# Verify the callback was updated and saved
|
|
conversation_callback.set_processor.assert_called_once_with(
|
|
slack_callback_processor
|
|
)
|
|
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.slack_manager'
|
|
)
|
|
async def test_send_message_to_slack(
|
|
self, mock_slack_manager, slack_callback_processor
|
|
):
|
|
"""Test the _send_message_to_slack method."""
|
|
# Setup mocks
|
|
mock_slack_user = MagicMock()
|
|
mock_saas_user_auth = MagicMock()
|
|
mock_slack_view = MagicMock()
|
|
mock_outgoing_message = MagicMock()
|
|
|
|
# Mock the authenticate_user method on slack_manager
|
|
mock_slack_manager.authenticate_user = AsyncMock(
|
|
return_value=(mock_slack_user, mock_saas_user_auth)
|
|
)
|
|
|
|
# Mock the SlackFactory
|
|
with patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.SlackFactory'
|
|
) as mock_slack_factory:
|
|
mock_slack_factory.create_slack_view_from_payload.return_value = (
|
|
mock_slack_view
|
|
)
|
|
mock_slack_manager.create_outgoing_message.return_value = (
|
|
mock_outgoing_message
|
|
)
|
|
mock_slack_manager.send_message = AsyncMock()
|
|
|
|
# Call the method
|
|
await slack_callback_processor._send_message_to_slack('Test message')
|
|
|
|
# Verify the behavior
|
|
mock_slack_manager.authenticate_user.assert_called_once_with(
|
|
slack_callback_processor.slack_user_id
|
|
)
|
|
|
|
# Check that the Message object was created correctly
|
|
message_call = mock_slack_factory.create_slack_view_from_payload.call_args[
|
|
0
|
|
][0]
|
|
assert isinstance(message_call, Message)
|
|
assert (
|
|
message_call.message['slack_user_id']
|
|
== slack_callback_processor.slack_user_id
|
|
)
|
|
assert (
|
|
message_call.message['channel_id']
|
|
== slack_callback_processor.channel_id
|
|
)
|
|
assert (
|
|
message_call.message['message_ts']
|
|
== slack_callback_processor.message_ts
|
|
)
|
|
assert (
|
|
message_call.message['thread_ts'] == slack_callback_processor.thread_ts
|
|
)
|
|
assert message_call.message['team_id'] == slack_callback_processor.team_id
|
|
|
|
# Verify the slack manager methods were called correctly
|
|
mock_slack_manager.create_outgoing_message.assert_called_once_with(
|
|
'Test message'
|
|
)
|
|
mock_slack_manager.send_message.assert_called_once_with(
|
|
mock_outgoing_message, mock_slack_view
|
|
)
|
|
|
|
@patch('server.conversation_callback_processor.slack_callback_processor.logger')
|
|
async def test_send_message_to_slack_exception(
|
|
self, mock_logger, slack_callback_processor
|
|
):
|
|
"""Test the _send_message_to_slack method when an exception occurs."""
|
|
# Setup mock to raise an exception
|
|
with patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.slack_manager'
|
|
) as mock_slack_manager:
|
|
mock_slack_manager.authenticate_user = AsyncMock(
|
|
side_effect=Exception('Test exception')
|
|
)
|
|
|
|
# Call the method
|
|
await slack_callback_processor._send_message_to_slack('Test message')
|
|
|
|
# Verify that the exception was caught and logged
|
|
mock_logger.error.assert_called_once()
|
|
assert (
|
|
'Failed to send summary message: Test exception'
|
|
in mock_logger.error.call_args[0][0]
|
|
)
|
|
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_summary_instruction'
|
|
)
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.conversation_manager'
|
|
)
|
|
@patch('server.conversation_callback_processor.slack_callback_processor.logger')
|
|
async def test_call_with_exception(
|
|
self,
|
|
mock_logger,
|
|
mock_conversation_manager,
|
|
mock_get_summary_instruction,
|
|
slack_callback_processor,
|
|
agent_state_changed_observation,
|
|
conversation_callback,
|
|
):
|
|
"""Test the __call__ method when an exception occurs."""
|
|
# Setup mock to raise an exception
|
|
mock_get_summary_instruction.side_effect = Exception('Test exception')
|
|
|
|
# Call the method
|
|
await slack_callback_processor(
|
|
callback=conversation_callback,
|
|
observation=agent_state_changed_observation,
|
|
)
|
|
|
|
# Verify that the exception was caught and logged
|
|
mock_logger.error.assert_called_once()
|
|
|
|
def test_model_validation(self):
|
|
"""Test the model validation of SlackCallbackProcessor."""
|
|
# Test with all required fields
|
|
processor = SlackCallbackProcessor(
|
|
slack_user_id='test_user',
|
|
channel_id='test_channel',
|
|
message_ts='test_message_ts',
|
|
thread_ts='test_thread_ts',
|
|
team_id='test_team_id',
|
|
)
|
|
assert processor.slack_user_id == 'test_user'
|
|
assert processor.channel_id == 'test_channel'
|
|
assert processor.message_ts == 'test_message_ts'
|
|
assert processor.thread_ts == 'test_thread_ts'
|
|
assert processor.team_id == 'test_team_id'
|
|
assert processor.last_user_msg_id is None
|
|
|
|
# Test with last_user_msg_id provided
|
|
processor_with_msg_id = SlackCallbackProcessor(
|
|
slack_user_id='test_user',
|
|
channel_id='test_channel',
|
|
message_ts='test_message_ts',
|
|
thread_ts='test_thread_ts',
|
|
team_id='test_team_id',
|
|
last_user_msg_id=456,
|
|
)
|
|
assert processor_with_msg_id.last_user_msg_id == 456
|
|
|
|
def test_serialization_deserialization(self):
|
|
"""Test serialization and deserialization of SlackCallbackProcessor."""
|
|
# Create a processor
|
|
original_processor = SlackCallbackProcessor(
|
|
slack_user_id='test_user',
|
|
channel_id='test_channel',
|
|
message_ts='test_message_ts',
|
|
thread_ts='test_thread_ts',
|
|
team_id='test_team_id',
|
|
last_user_msg_id=125,
|
|
)
|
|
|
|
# Serialize to JSON
|
|
json_data = original_processor.model_dump_json()
|
|
|
|
# Deserialize from JSON
|
|
deserialized_processor = SlackCallbackProcessor.model_validate_json(json_data)
|
|
|
|
# Verify fields match
|
|
assert deserialized_processor.slack_user_id == original_processor.slack_user_id
|
|
assert deserialized_processor.channel_id == original_processor.channel_id
|
|
assert deserialized_processor.message_ts == original_processor.message_ts
|
|
assert deserialized_processor.thread_ts == original_processor.thread_ts
|
|
assert deserialized_processor.team_id == original_processor.team_id
|
|
assert (
|
|
deserialized_processor.last_user_msg_id
|
|
== original_processor.last_user_msg_id
|
|
)
|
|
|
|
@patch(
|
|
'server.conversation_callback_processor.slack_callback_processor.get_last_user_msg_from_conversation_manager'
|
|
)
|
|
@patch('server.conversation_callback_processor.slack_callback_processor.logger')
|
|
async def test_call_with_unchanged_message_id(
|
|
self,
|
|
mock_logger,
|
|
mock_get_last_user_msg,
|
|
slack_callback_processor,
|
|
agent_state_changed_observation,
|
|
conversation_callback,
|
|
):
|
|
"""Test the __call__ method when the message ID hasn't changed."""
|
|
# Setup - simulate that the message ID hasn't changed
|
|
mock_last_msg = MagicMock()
|
|
mock_last_msg.id = 123
|
|
mock_last_msg.content = 'Hello'
|
|
mock_get_last_user_msg.return_value = [mock_last_msg]
|
|
|
|
# Set the last_user_msg_id to the same value
|
|
slack_callback_processor.last_user_msg_id = 123
|
|
|
|
# Call the method
|
|
await slack_callback_processor(
|
|
callback=conversation_callback,
|
|
observation=agent_state_changed_observation,
|
|
)
|
|
|
|
# Verify that the method returned early and no further processing was done
|
|
# Make sure we didn't update the processor or save to the database
|
|
conversation_callback.set_processor.assert_not_called()
|
|
|
|
def test_integration_with_conversation_callback(self):
|
|
"""Test integration with ConversationCallback."""
|
|
# Create a processor
|
|
processor = SlackCallbackProcessor(
|
|
slack_user_id='test_user',
|
|
channel_id='test_channel',
|
|
message_ts='test_message_ts',
|
|
thread_ts='test_thread_ts',
|
|
team_id='test_team_id',
|
|
)
|
|
|
|
# Set the processor on the callback
|
|
callback = ConversationCallback()
|
|
callback.set_processor(processor)
|
|
|
|
# Verify set_processor was called with the correct processor type
|
|
expected_processor_type = (
|
|
f'{SlackCallbackProcessor.__module__}.{SlackCallbackProcessor.__name__}'
|
|
)
|
|
assert callback.processor_type == expected_processor_type
|
|
|
|
# Verify processor_json contains the serialized processor
|
|
assert 'slack_user_id' in callback.processor_json
|
|
assert 'channel_id' in callback.processor_json
|
|
assert 'message_ts' in callback.processor_json
|
|
assert 'thread_ts' in callback.processor_json
|
|
assert 'team_id' in callback.processor_json
|