scratch_chat / tests /unit /test_chat_history.py
WebashalarForML's picture
Upload 178 files
330b6e4 verified
"""
Unit tests for ChatHistoryManager service.
Tests cover message storage, retrieval, caching, cache synchronization,
and error handling scenarios for the dual storage system.
"""
import pytest
import json
from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock
from uuid import uuid4
from chat_agent.services.chat_history import (
ChatHistoryManager,
ChatHistoryError,
create_chat_history_manager
)
from chat_agent.models.message import Message
class TestChatHistoryManager:
"""Test suite for ChatHistoryManager class"""
@pytest.fixture
def mock_redis_client(self):
"""Mock Redis client for testing"""
mock_redis = Mock()
mock_redis.rpush = Mock()
mock_redis.lrange = Mock()
mock_redis.llen = Mock()
mock_redis.ltrim = Mock()
mock_redis.delete = Mock()
mock_redis.expire = Mock()
mock_redis.ttl = Mock()
return mock_redis
@pytest.fixture
def chat_history_manager(self, mock_redis_client):
"""Create ChatHistoryManager instance for testing"""
return ChatHistoryManager(
redis_client=mock_redis_client,
max_cache_messages=20,
context_window_size=10
)
@pytest.fixture
def sample_message_data(self):
"""Sample message data for testing"""
session_id = str(uuid4())
return {
'session_id': session_id,
'role': 'user',
'content': 'Hello, can you help me with Python?',
'language': 'python',
'message_metadata': {'test': 'data'}
}
@pytest.fixture
def mock_message(self, sample_message_data):
"""Mock Message instance"""
message = Mock(spec=Message)
message.id = str(uuid4())
message.session_id = sample_message_data['session_id']
message.role = sample_message_data['role']
message.content = sample_message_data['content']
message.language = sample_message_data['language']
message.timestamp = datetime.utcnow()
message.message_metadata = sample_message_data['message_metadata']
return message
@pytest.fixture
def mock_messages_list(self, sample_message_data):
"""Mock list of Message instances"""
messages = []
for i in range(5):
message = Mock(spec=Message)
message.id = str(uuid4())
message.session_id = sample_message_data['session_id']
message.role = 'user' if i % 2 == 0 else 'assistant'
message.content = f'Message {i}'
message.language = 'python'
message.timestamp = datetime.utcnow() - timedelta(minutes=i)
message.message_metadata = {}
messages.append(message)
return messages
class TestMessageStorage:
"""Test message storage functionality"""
@patch('chat_agent.services.chat_history.db')
@patch('chat_agent.services.chat_history.Message')
def test_store_user_message_success(self, mock_message_class, mock_db,
chat_history_manager, mock_redis_client,
sample_message_data, mock_message):
"""Test successful user message storage"""
# Setup
mock_message_class.create_user_message.return_value = mock_message
# Execute
result = chat_history_manager.store_message(
session_id=sample_message_data['session_id'],
role='user',
content=sample_message_data['content'],
language=sample_message_data['language']
)
# Verify
assert result == mock_message
mock_message_class.create_user_message.assert_called_once_with(
sample_message_data['session_id'],
sample_message_data['content'],
sample_message_data['language']
)
mock_db.session.add.assert_called_once_with(mock_message)
mock_db.session.commit.assert_called_once()
mock_redis_client.rpush.assert_called_once()
mock_redis_client.expire.assert_called_once()
@patch('chat_agent.services.chat_history.db')
@patch('chat_agent.services.chat_history.Message')
def test_store_assistant_message_success(self, mock_message_class, mock_db,
chat_history_manager, mock_redis_client,
sample_message_data, mock_message):
"""Test successful assistant message storage"""
# Setup
mock_message_class.create_assistant_message.return_value = mock_message
# Execute
result = chat_history_manager.store_message(
session_id=sample_message_data['session_id'],
role='assistant',
content=sample_message_data['content'],
language=sample_message_data['language'],
message_metadata=sample_message_data['message_metadata']
)
# Verify
assert result == mock_message
mock_message_class.create_assistant_message.assert_called_once_with(
sample_message_data['session_id'],
sample_message_data['content'],
sample_message_data['language'],
sample_message_data['message_metadata']
)
mock_db.session.add.assert_called_once_with(mock_message)
mock_db.session.commit.assert_called_once()
@patch('chat_agent.services.chat_history.db')
def test_store_message_invalid_role(self, mock_db, chat_history_manager, sample_message_data):
"""Test storing message with invalid role"""
# Execute & Verify
with pytest.raises(ChatHistoryError, match="Failed to store message"):
chat_history_manager.store_message(
session_id=sample_message_data['session_id'],
role='invalid_role',
content=sample_message_data['content']
)
@patch('chat_agent.services.chat_history.db')
@patch('chat_agent.services.chat_history.Message')
def test_store_message_database_error(self, mock_message_class, mock_db,
chat_history_manager, sample_message_data, mock_message):
"""Test message storage with database error"""
# Setup
from sqlalchemy.exc import SQLAlchemyError
mock_message_class.create_user_message.return_value = mock_message
mock_db.session.add.side_effect = SQLAlchemyError("Database error")
# Execute & Verify
with pytest.raises(ChatHistoryError, match="Failed to store message"):
chat_history_manager.store_message(
session_id=sample_message_data['session_id'],
role='user',
content=sample_message_data['content']
)
mock_db.session.rollback.assert_called_once()
@patch('chat_agent.services.chat_history.db')
@patch('chat_agent.services.chat_history.Message')
def test_store_message_redis_error(self, mock_message_class, mock_db,
chat_history_manager, mock_redis_client,
sample_message_data, mock_message):
"""Test message storage with Redis error (should still succeed)"""
# Setup
import redis
mock_message_class.create_user_message.return_value = mock_message
mock_redis_client.rpush.side_effect = redis.RedisError("Redis error")
# Execute
result = chat_history_manager.store_message(
session_id=sample_message_data['session_id'],
role='user',
content=sample_message_data['content']
)
# Verify - should still succeed despite Redis error
assert result == mock_message
mock_db.session.add.assert_called_once_with(mock_message)
mock_db.session.commit.assert_called_once()
class TestMessageRetrieval:
"""Test message retrieval functionality"""
@patch('chat_agent.services.chat_history.db')
def test_get_recent_history_from_cache(self, mock_db, chat_history_manager,
mock_redis_client, sample_message_data):
"""Test getting recent history from cache"""
# Setup
session_id = sample_message_data['session_id']
cached_messages = [
json.dumps({
'id': str(uuid4()),
'session_id': session_id,
'role': 'user',
'content': 'Test message',
'language': 'python',
'timestamp': datetime.utcnow().isoformat(),
'message_metadata': {}
})
]
mock_redis_client.lrange.return_value = cached_messages
# Execute
result = chat_history_manager.get_recent_history(session_id, limit=5)
# Verify
assert len(result) == 1
assert result[0].content == 'Test message'
mock_redis_client.lrange.assert_called_once_with(f"chat_history:{session_id}", -5, -1)
# Should not query database when cache has enough messages
mock_db.session.query.assert_not_called()
@patch('chat_agent.services.chat_history.db')
def test_get_recent_history_from_database(self, mock_db, chat_history_manager,
mock_redis_client, sample_message_data, mock_messages_list):
"""Test getting recent history from database when cache is empty"""
# Setup
session_id = sample_message_data['session_id']
mock_redis_client.lrange.return_value = []
# Mock database query
mock_query = Mock()
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.all.return_value = mock_messages_list
mock_db.session.query.return_value = mock_query
# Execute
result = chat_history_manager.get_recent_history(session_id, limit=5)
# Verify
assert len(result) == 5
mock_db.session.query.assert_called_once()
mock_redis_client.lrange.assert_called_once()
@patch('chat_agent.services.chat_history.db')
def test_get_recent_history_database_error(self, mock_db, chat_history_manager,
mock_redis_client, sample_message_data):
"""Test getting recent history with database error"""
# Setup
from sqlalchemy.exc import SQLAlchemyError
session_id = sample_message_data['session_id']
mock_redis_client.lrange.return_value = []
mock_db.session.query.side_effect = SQLAlchemyError("Database error")
# Execute & Verify
with pytest.raises(ChatHistoryError, match="Failed to get recent history"):
chat_history_manager.get_recent_history(session_id)
@patch('chat_agent.services.chat_history.db')
def test_get_full_history_success(self, mock_db, chat_history_manager,
sample_message_data, mock_messages_list):
"""Test getting full history with pagination"""
# Setup
session_id = sample_message_data['session_id']
# Mock database query
mock_query = Mock()
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.all.return_value = mock_messages_list
mock_db.session.query.return_value = mock_query
# Execute
result = chat_history_manager.get_full_history(session_id, page=2, page_size=10)
# Verify
assert len(result) == 5
mock_query.offset.assert_called_once_with(10) # (page-1) * page_size
mock_query.limit.assert_called_once_with(10)
@patch('chat_agent.services.chat_history.db')
def test_get_message_count_success(self, mock_db, chat_history_manager, sample_message_data):
"""Test getting message count for a session"""
# Setup
session_id = sample_message_data['session_id']
mock_query = Mock()
mock_query.filter.return_value = mock_query
mock_query.count.return_value = 15
mock_db.session.query.return_value = mock_query
# Execute
result = chat_history_manager.get_message_count(session_id)
# Verify
assert result == 15
mock_db.session.query.assert_called_once()
class TestCacheSynchronization:
"""Test cache synchronization functionality"""
def test_cache_message_success(self, chat_history_manager, mock_redis_client, mock_message):
"""Test caching a single message"""
# Execute
chat_history_manager._cache_message(mock_message)
# Verify
mock_redis_client.rpush.assert_called_once()
mock_redis_client.expire.assert_called_once_with(f"chat_history:{mock_message.session_id}", 86400)
def test_cache_message_redis_error(self, chat_history_manager, mock_redis_client, mock_message):
"""Test caching message with Redis error"""
# Setup
import redis
mock_redis_client.rpush.side_effect = redis.RedisError("Redis error")
# Execute - should not raise exception
chat_history_manager._cache_message(mock_message)
# Verify - error should be logged but not raised
mock_redis_client.rpush.assert_called_once()
def test_cache_messages_success(self, chat_history_manager, mock_redis_client, mock_messages_list):
"""Test caching multiple messages"""
# Execute
chat_history_manager._cache_messages(mock_messages_list)
# Verify
mock_redis_client.delete.assert_called_once()
mock_redis_client.rpush.assert_called_once()
mock_redis_client.expire.assert_called_once()
def test_trim_cache_success(self, chat_history_manager, mock_redis_client, sample_message_data):
"""Test trimming cache to maintain size limit"""
# Setup
session_id = sample_message_data['session_id']
# Execute
chat_history_manager._trim_cache(session_id)
# Verify
mock_redis_client.ltrim.assert_called_once_with(f"chat_history:{session_id}", -20, -1)
def test_clear_cache_success(self, chat_history_manager, mock_redis_client, sample_message_data):
"""Test clearing cache for a session"""
# Setup
session_id = sample_message_data['session_id']
# Execute
chat_history_manager._clear_cache(session_id)
# Verify
mock_redis_client.delete.assert_called_once_with(f"chat_history:{session_id}")
class TestHistoryManagement:
"""Test history management operations"""
@patch('chat_agent.services.chat_history.db')
def test_clear_session_history_success(self, mock_db, chat_history_manager,
mock_redis_client, sample_message_data):
"""Test clearing all history for a session"""
# Setup
session_id = sample_message_data['session_id']
# Mock message count query
mock_count_query = Mock()
mock_count_query.filter.return_value = mock_count_query
mock_count_query.count.return_value = 5
# Mock delete query
mock_delete_query = Mock()
mock_delete_query.filter.return_value = mock_delete_query
mock_delete_query.delete.return_value = 5
mock_db.session.query.side_effect = [mock_count_query, mock_delete_query]
# Execute
result = chat_history_manager.clear_session_history(session_id)
# Verify
assert result == 5
mock_db.session.commit.assert_called_once()
mock_redis_client.delete.assert_called_once_with(f"chat_history:{session_id}")
@patch('chat_agent.services.chat_history.db')
def test_search_messages_success(self, mock_db, chat_history_manager,
sample_message_data, mock_messages_list):
"""Test searching messages by content"""
# Setup
session_id = sample_message_data['session_id']
query = "python"
# Mock database query
mock_query = Mock()
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.all.return_value = mock_messages_list[:2] # Return 2 matching messages
mock_db.session.query.return_value = mock_query
# Execute
result = chat_history_manager.search_messages(session_id, query, limit=20)
# Verify
assert len(result) == 2
mock_db.session.query.assert_called_once()
def test_get_cache_stats_success(self, chat_history_manager, mock_redis_client, sample_message_data):
"""Test getting cache statistics"""
# Setup
session_id = sample_message_data['session_id']
mock_redis_client.llen.return_value = 10
mock_redis_client.ttl.return_value = 3600
# Execute
result = chat_history_manager.get_cache_stats(session_id)
# Verify
assert result['session_id'] == session_id
assert result['cached_messages'] == 10
assert result['cache_ttl'] == 3600
assert result['max_cache_size'] == 20
def test_get_cache_stats_redis_error(self, chat_history_manager, mock_redis_client, sample_message_data):
"""Test getting cache statistics with Redis error"""
# Setup
import redis
session_id = sample_message_data['session_id']
mock_redis_client.llen.side_effect = redis.RedisError("Redis error")
# Execute
result = chat_history_manager.get_cache_stats(session_id)
# Verify
assert result['session_id'] == session_id
assert result['cached_messages'] == 0
assert result['cache_ttl'] == -1
assert 'error' in result
class TestFactoryFunction:
"""Test factory function for creating ChatHistoryManager"""
def test_create_chat_history_manager(self, mock_redis_client):
"""Test factory function creates manager with correct configuration"""
# Execute
manager = create_chat_history_manager(
redis_client=mock_redis_client,
max_cache_messages=30,
context_window_size=15
)
# Verify
assert isinstance(manager, ChatHistoryManager)
assert manager.redis_client == mock_redis_client
assert manager.max_cache_messages == 30
assert manager.context_window_size == 15
assert manager.cache_prefix == "chat_history:"