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

185 lines
6.7 KiB
Python

"""
Tests for ConversationCallbackProcessor and ConversationCallback models.
"""
import json
import pytest
from storage.conversation_callback import (
CallbackStatus,
ConversationCallback,
ConversationCallbackProcessor,
)
from storage.stored_conversation_metadata import StoredConversationMetadata
from openhands.events.observation.agent import AgentStateChangedObservation
class MockConversationCallbackProcessor(ConversationCallbackProcessor):
"""Mock implementation of ConversationCallbackProcessor for testing."""
name: str = 'test'
config: dict = {}
def __init__(self, name: str = 'test', config: dict | None = None, **kwargs):
super().__init__(name=name, config=config or {}, **kwargs)
self.call_count = 0
self.last_conversation_id: str | None = None
def __call__(
self, callback: ConversationCallback, observation: AgentStateChangedObservation
) -> None:
"""Mock implementation that tracks calls."""
self.call_count += 1
self.last_conversation_id = callback.conversation_id
class TestConversationCallbackProcessor:
"""Test the ConversationCallbackProcessor abstract base class."""
def test_mock_processor_creation(self):
"""Test that we can create a mock processor."""
processor = MockConversationCallbackProcessor(
name='test_processor', config={'key': 'value'}
)
assert processor.name == 'test_processor'
assert processor.config == {'key': 'value'}
assert processor.call_count == 0
assert processor.last_conversation_id is None
def test_mock_processor_call(self):
"""Test that the mock processor can be called."""
callback = ConversationCallback(conversation_id='test_conversation_id')
processor = MockConversationCallbackProcessor()
processor(
callback,
AgentStateChangedObservation('foobar', 'awaiting_user_input'),
)
assert processor.call_count == 1
assert processor.last_conversation_id == 'test_conversation_id'
def test_processor_serialization(self):
"""Test that processors can be serialized to JSON."""
processor = MockConversationCallbackProcessor(
name='test', config={'setting': 'value'}
)
json_data = processor.model_dump_json()
# Should be able to parse the JSON
data = json.loads(json_data)
assert data['name'] == 'test'
assert data['config'] == {'setting': 'value'}
class TestConversationCallback:
"""Test the ConversationCallback SQLAlchemy model."""
@pytest.fixture
def conversation_metadata(self, session_maker):
"""Create a test conversation metadata record."""
with session_maker() as session:
metadata = StoredConversationMetadata(
conversation_id='test_conversation_123', user_id='test_user_456'
)
session.add(metadata)
session.commit()
session.refresh(metadata)
yield metadata
# Cleanup
session.delete(metadata)
session.commit()
def test_callback_creation(self, conversation_metadata, session_maker):
"""Test creating a conversation callback."""
processor = MockConversationCallbackProcessor(name='test_processor')
with session_maker() as session:
callback = ConversationCallback(
conversation_id=conversation_metadata.conversation_id,
status=CallbackStatus.ACTIVE,
processor_type='tests.unit.test_conversation_processor.MockConversationCallbackProcessor',
processor_json=processor.model_dump_json(),
)
session.add(callback)
session.commit()
session.refresh(callback)
assert callback.id is not None
assert callback.conversation_id == conversation_metadata.conversation_id
assert callback.status == CallbackStatus.ACTIVE
assert callback.created_at is not None
assert callback.updated_at is not None
# Cleanup
session.delete(callback)
session.commit()
def test_set_processor(self, conversation_metadata, session_maker):
"""Test setting a processor on a callback."""
processor = MockConversationCallbackProcessor(
name='test_processor', config={'key': 'value'}
)
callback = ConversationCallback(
conversation_id=conversation_metadata.conversation_id
)
callback.set_processor(processor)
assert (
callback.processor_type
== 'enterprise.tests.unit.test_conversation_callback_processor.MockConversationCallbackProcessor'
)
# Verify the JSON contains the processor data
processor_data = json.loads(callback.processor_json)
assert processor_data['name'] == 'test_processor'
assert processor_data['config'] == {'key': 'value'}
def test_get_processor(self, conversation_metadata, session_maker):
"""Test getting a processor from a callback."""
processor = MockConversationCallbackProcessor(
name='test_processor', config={'key': 'value'}
)
callback = ConversationCallback(
conversation_id=conversation_metadata.conversation_id
)
callback.set_processor(processor)
# Get the processor back
retrieved_processor = callback.get_processor()
assert isinstance(retrieved_processor, MockConversationCallbackProcessor)
assert retrieved_processor.name == 'test_processor'
assert retrieved_processor.config == {'key': 'value'}
def test_callback_status_enum(self):
"""Test the CallbackStatus enum."""
assert CallbackStatus.ACTIVE.value == 'ACTIVE'
assert CallbackStatus.COMPLETED.value == 'COMPLETED'
assert CallbackStatus.ERROR.value == 'ERROR'
def test_callback_foreign_key_constraint(
self, conversation_metadata, session_maker
):
"""Test that the foreign key constraint works."""
with session_maker() as session:
# This should work with valid conversation_id
callback = ConversationCallback(
conversation_id=conversation_metadata.conversation_id,
processor_type='test.Processor',
processor_json='{}',
)
session.add(callback)
session.commit()
# Cleanup
session.delete(callback)
session.commit()
# Note: SQLite doesn't enforce foreign key constraints by default in tests
# In a real PostgreSQL database, this would raise an integrity error
# For now, we just test that the callback can be created with valid data