OpenHands/enterprise/tests/unit/test_gitlab_callback_processor.py
2025-09-04 15:44:54 -04:00

233 lines
8.7 KiB
Python

"""
Tests for the GitlabCallbackProcessor.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from integrations.gitlab.gitlab_view import GitlabIssueComment
from integrations.types import UserData
from server.conversation_callback_processor.gitlab_callback_processor import (
GitlabCallbackProcessor,
)
from storage.conversation_callback import CallbackStatus, ConversationCallback
from openhands.core.schema.agent import AgentState
from openhands.events.observation.agent import AgentStateChangedObservation
@pytest.fixture
def mock_gitlab_view():
"""Create a mock GitlabViewType for testing."""
# Use a simple dict that matches GitlabIssue structure
return GitlabIssueComment(
installation_id='test_installation',
issue_number=789,
project_id=456,
full_repo_name='test/repo',
is_public_repo=True,
user_info=UserData(
user_id=123, username='test_user', keycloak_user_id='test_keycloak_id'
),
raw_payload={'source': 'gitlab', 'message': {'test': 'data'}},
conversation_id='test_conversation',
should_extract=True,
send_summary_instruction=True,
title='',
description='',
previous_comments=[],
is_mr=False,
comment_body='sdfs',
discussion_id='test_discussion',
confidential=False,
)
@pytest.fixture
def gitlab_callback_processor(mock_gitlab_view):
"""Create a GitlabCallbackProcessor instance for testing."""
return GitlabCallbackProcessor(
gitlab_view=mock_gitlab_view,
send_summary_instruction=True,
)
class TestGitlabCallbackProcessor:
"""Test the GitlabCallbackProcessor class."""
def test_model_validation(self, mock_gitlab_view):
"""Test the model validation of GitlabCallbackProcessor."""
# Test with all required fields
processor = GitlabCallbackProcessor(
gitlab_view=mock_gitlab_view,
)
# Check that gitlab_view was converted to a GitlabIssue object
assert hasattr(processor.gitlab_view, 'issue_number')
assert processor.gitlab_view.issue_number == 789
assert processor.gitlab_view.full_repo_name == 'test/repo'
assert processor.send_summary_instruction is True
# Test with custom send_summary_instruction
processor = GitlabCallbackProcessor(
gitlab_view=mock_gitlab_view,
send_summary_instruction=False,
)
assert hasattr(processor.gitlab_view, 'issue_number')
assert processor.gitlab_view.issue_number == 789
assert processor.send_summary_instruction is False
def test_serialization(self, mock_gitlab_view):
"""Test serialization and deserialization of GitlabCallbackProcessor."""
original_processor = GitlabCallbackProcessor(
gitlab_view=mock_gitlab_view,
send_summary_instruction=True,
)
# Serialize to JSON
json_data = original_processor.model_dump_json()
assert isinstance(json_data, str)
# Deserialize from JSON
deserialized_processor = GitlabCallbackProcessor.model_validate_json(json_data)
assert (
deserialized_processor.send_summary_instruction
== original_processor.send_summary_instruction
)
assert (
deserialized_processor.gitlab_view.issue_number
== original_processor.gitlab_view.issue_number
)
assert isinstance(
deserialized_processor.gitlab_view.issue_number,
type(original_processor.gitlab_view.issue_number),
)
# Note: gitlab_view will be serialized as a dict, so we can't directly compare objects
@pytest.mark.asyncio
@patch(
'server.conversation_callback_processor.gitlab_callback_processor.get_summary_instruction'
)
@patch(
'server.conversation_callback_processor.gitlab_callback_processor.conversation_manager'
)
@patch(
'server.conversation_callback_processor.gitlab_callback_processor.session_maker'
)
async def test_call_with_send_summary_instruction(
self,
mock_session_maker,
mock_conversation_manager,
mock_get_summary_instruction,
gitlab_callback_processor,
):
"""Test the __call__ method when send_summary_instruction is True."""
# Setup mocks
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_conversation_manager.send_event_to_conversation = AsyncMock()
mock_get_summary_instruction.return_value = (
"I'm a man of few words. Any questions?"
)
# Create a callback and observation
callback = ConversationCallback(
conversation_id='conv123',
status=CallbackStatus.ACTIVE,
processor_type=f'{GitlabCallbackProcessor.__module__}.{GitlabCallbackProcessor.__name__}',
processor_json=gitlab_callback_processor.model_dump_json(),
)
observation = AgentStateChangedObservation(
content='', agent_state=AgentState.AWAITING_USER_INPUT
)
# Call the processor
await gitlab_callback_processor(callback, observation)
# Verify that send_event_to_conversation was called
mock_conversation_manager.send_event_to_conversation.assert_called_once()
# Verify that the processor state was updated
assert gitlab_callback_processor.send_summary_instruction is False
mock_session.merge.assert_called_once_with(callback)
mock_session.commit.assert_called_once()
@pytest.mark.asyncio
@patch(
'server.conversation_callback_processor.gitlab_callback_processor.conversation_manager'
)
@patch(
'server.conversation_callback_processor.gitlab_callback_processor.extract_summary_from_conversation_manager'
)
@patch(
'server.conversation_callback_processor.gitlab_callback_processor.asyncio.create_task'
)
@patch(
'server.conversation_callback_processor.gitlab_callback_processor.session_maker'
)
async def test_call_with_extract_summary(
self,
mock_session_maker,
mock_create_task,
mock_extract_summary,
mock_conversation_manager,
gitlab_callback_processor,
):
"""Test the __call__ method when send_summary_instruction is False."""
# Setup mocks
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_extract_summary.return_value = 'Test summary'
# Ensure we don't leak an un-awaited coroutine when create_task is mocked
mock_create_task.side_effect = lambda coro: (coro.close(), None)[1]
# Set send_summary_instruction to False
gitlab_callback_processor.send_summary_instruction = False
# Create a callback and observation
callback = ConversationCallback(
conversation_id='conv123',
status=CallbackStatus.ACTIVE,
processor_type=f'{GitlabCallbackProcessor.__module__}.{GitlabCallbackProcessor.__name__}',
processor_json=gitlab_callback_processor.model_dump_json(),
)
observation = AgentStateChangedObservation(
content='', agent_state=AgentState.FINISHED
)
# Call the processor
await gitlab_callback_processor(callback, observation)
# Verify that extract_summary_from_conversation_manager was called
mock_extract_summary.assert_called_once_with(
mock_conversation_manager, 'conv123'
)
# Verify that create_task was called to send the message
mock_create_task.assert_called_once()
# Verify that the callback status was updated
assert callback.status == CallbackStatus.COMPLETED
mock_session.merge.assert_called_once_with(callback)
mock_session.commit.assert_called_once()
@pytest.mark.asyncio
async def test_call_with_non_terminal_state(self, gitlab_callback_processor):
"""Test the __call__ method with a non-terminal agent state."""
# Create a callback and observation with a non-terminal state
callback = ConversationCallback(
conversation_id='conv123',
status=CallbackStatus.ACTIVE,
processor_type=f'{GitlabCallbackProcessor.__module__}.{GitlabCallbackProcessor.__name__}',
processor_json=gitlab_callback_processor.model_dump_json(),
)
observation = AgentStateChangedObservation(
content='', agent_state=AgentState.RUNNING
)
# Call the processor
await gitlab_callback_processor(callback, observation)
# Verify that nothing happened (early return)
assert gitlab_callback_processor.send_summary_instruction is True