Spaces:
Runtime error
Runtime error
| """ | |
| 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""" | |
| 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 | |
| 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 | |
| ) | |
| 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'} | |
| } | |
| 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 | |
| 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""" | |
| 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() | |
| 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() | |
| 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'] | |
| ) | |
| 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() | |
| 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""" | |
| 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() | |
| 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() | |
| 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) | |
| 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) | |
| 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""" | |
| 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}") | |
| 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:" |