blob: 12cdae47fe6dc815f9fb5c60691fd2b37bb4a447 [file] [log] [blame] [raw]
"""
Claude Agent适配器扩展测试
V2.2 重构 - 基于 claude-agent-sdk 的简化 API
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch, mock_open
import asyncio
import tempfile
import shutil
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
from claude_agent.telegram.claude_adapter import ClaudeAgentAdapter
class TestClaudeAgentAdapterExtended:
"""Claude Agent适配器扩展测试 - V2.2"""
@pytest.fixture
def mock_agent(self):
"""创建Mock AgentCore"""
agent = Mock()
# Mock streaming response
async def mock_streaming_response(prompt):
yield "Mock"
yield " agent"
yield " response"
agent.create_streaming_response = AsyncMock(side_effect=lambda *args: mock_streaming_response(args[0]))
agent.process_user_input = AsyncMock(return_value="Mock agent response")
agent.conversation_history = []
agent.to_dict = Mock(return_value={'test': 'data'})
agent.clean_conversation_history = Mock(return_value=0)
agent.trim_conversation_history = Mock(return_value=0)
agent.set_thinking_mode = Mock()
return agent
@pytest.fixture
def adapter_with_mock(self, mock_agent):
"""创建带Mock Agent的适配器"""
with patch('claude_agent.telegram.claude_adapter.PersistenceManager') as mock_persistence:
mock_persistence.return_value.load_agent_state.return_value = None
mock_persistence.return_value.save_agent_state.return_value = True
mock_persistence.return_value.save_conversation_history.return_value = True
mock_persistence.return_value.get_all_chat_ids.return_value = []
adapter = ClaudeAgentAdapter()
test_chat_id = 'test_chat'
adapter._agents[test_chat_id] = mock_agent
return adapter
@pytest.fixture
def adapter_without_agent(self):
"""创建没有预设Agent的适配器"""
with patch('claude_agent.telegram.claude_adapter.AgentCore') as mock_core:
mock_instance = Mock()
mock_instance.process_user_input = AsyncMock(return_value="Default agent response")
mock_instance.conversation_history = []
mock_instance.to_dict = Mock(return_value={})
mock_instance.clean_conversation_history = Mock(return_value=0)
mock_instance.trim_conversation_history = Mock(return_value=0)
mock_core.return_value = mock_instance
mock_core.from_dict.return_value = mock_instance
with patch('claude_agent.telegram.claude_adapter.PersistenceManager') as mock_pm:
mock_pm.return_value.get_all_chat_ids.return_value = []
mock_pm.return_value.load_agent_state.return_value = None
return ClaudeAgentAdapter()
@pytest.fixture
def sample_context(self):
"""示例对话上下文"""
return [
{'message': 'Hello', 'is_bot': False, 'timestamp': '2024-01-01T10:00:00'},
{'message': 'Hi! How can I help?', 'is_bot': True, 'timestamp': '2024-01-01T10:00:01'},
{'message': 'I need help', 'is_bot': False, 'timestamp': '2024-01-01T10:00:02'}
]
@pytest.fixture
def sample_user_info(self):
"""示例用户信息"""
return {
'username': 'testuser',
'first_name': 'Test',
'last_name': 'User',
'language_code': 'en',
'chat_type': 'private',
'chat_id': 'test_chat'
}
@pytest.mark.asyncio
async def test_process_message_success(self, mock_agent, sample_context, sample_user_info):
"""测试成功处理消息"""
with patch('claude_agent.telegram.claude_adapter.PersistenceManager') as mock_persistence:
mock_persistence.return_value.load_agent_state.return_value = None
mock_persistence.return_value.save_agent_state.return_value = True
mock_persistence.return_value.save_conversation_history.return_value = True
mock_persistence.return_value.get_all_chat_ids.return_value = []
adapter = ClaudeAgentAdapter()
test_chat_id = 'test_chat'
adapter._agents[test_chat_id] = mock_agent
message = "Test message"
result = await adapter.process_message(message, sample_context, sample_user_info)
mock_agent.process_user_input.assert_called_once()
assert result == "Mock agent response"
@pytest.mark.asyncio
async def test_process_message_agent_error(self, adapter_with_mock, mock_agent, sample_context, sample_user_info):
"""测试Agent处理错误"""
mock_agent.process_user_input.side_effect = Exception("Agent error")
result = await adapter_with_mock.process_message("Test", sample_context, sample_user_info)
assert "主人" in result
@pytest.mark.asyncio
async def test_process_with_image(self, adapter_with_mock, mock_agent, sample_context, sample_user_info):
"""测试处理图片消息"""
message = "What's in this image?"
image_path = "/tmp/test_image.jpg"
result = await adapter_with_mock.process_with_image(message, image_path, sample_context, sample_user_info)
mock_agent.process_user_input.assert_called_once()
call_args = mock_agent.process_user_input.call_args[0][0]
assert image_path in call_args
assert message in call_args
assert "用户发送了一张图片" in call_args
assert result == "Mock agent response"
@pytest.mark.asyncio
async def test_process_with_image_error(self, adapter_with_mock, mock_agent, sample_context, sample_user_info):
"""测试处理图片消息时出错"""
mock_agent.process_user_input.side_effect = Exception("Image processing error")
result = await adapter_with_mock.process_with_image("Test", "/tmp/img.jpg", sample_context, sample_user_info)
assert "主人" in result
@pytest.mark.asyncio
async def test_process_with_document(self, adapter_with_mock, mock_agent, sample_context, sample_user_info):
"""测试处理文档消息"""
message = "Analyze this document"
document_path = "/tmp/test_doc.pdf"
result = await adapter_with_mock.process_with_document(message, document_path, sample_context, sample_user_info)
mock_agent.process_user_input.assert_called_once()
call_args = mock_agent.process_user_input.call_args[0][0]
assert document_path in call_args
assert message in call_args
assert "用户发送了一个文档" in call_args
assert result == "Mock agent response"
@pytest.mark.asyncio
async def test_process_with_document_error(self, adapter_with_mock, mock_agent, sample_context, sample_user_info):
"""测试处理文档消息时出错"""
mock_agent.process_user_input.side_effect = Exception("Document processing error")
result = await adapter_with_mock.process_with_document("Test", "/tmp/doc.pdf", sample_context, sample_user_info)
assert "主人" in result
@pytest.mark.asyncio
async def test_process_yolo_mode(self, adapter_with_mock, mock_agent, sample_context, sample_user_info):
"""测试YOLO模式处理"""
message = "Help me solve this complex problem"
result = await adapter_with_mock.process_yolo_mode(message, sample_context, sample_user_info)
# 验证设置了YOLO模式
mock_agent.set_thinking_mode.assert_called()
mock_agent.process_user_input.assert_called_once()
assert result == "Mock agent response"
@pytest.mark.asyncio
async def test_process_yolo_mode_error(self, adapter_with_mock, mock_agent, sample_context, sample_user_info):
"""测试YOLO模式处理错误"""
mock_agent.process_user_input.side_effect = Exception("YOLO processing error")
result = await adapter_with_mock.process_yolo_mode("Test", sample_context, sample_user_info)
assert "主人" in result
def test_build_user_context_complete_info(self, adapter_with_mock, sample_user_info):
"""测试构建用户上下文(完整信息)"""
result = adapter_with_mock._build_user_context(sample_user_info)
assert "用户信息:" in result
assert "用户名: @testuser" in result
assert "名字: Test" in result
assert "姓氏: User" in result
assert "语言: en" in result
assert "这是私人对话" in result
def test_build_user_context_minimal_info(self, adapter_with_mock):
"""测试构建用户上下文(最小信息)"""
minimal_info = {'chat_type': 'group'}
result = adapter_with_mock._build_user_context(minimal_info)
assert "用户信息:" in result
assert "这是群组对话" in result
def test_build_user_context_different_chat_types(self, adapter_with_mock):
"""测试不同聊天类型的用户上下文"""
chat_types = {
'group': '这是群组对话',
'supergroup': '这是超级群组对话',
'channel': '这是频道消息',
'private': '这是私人对话'
}
for chat_type, expected_text in chat_types.items():
user_info = {'chat_type': chat_type}
result = adapter_with_mock._build_user_context(user_info)
assert expected_text in result
def test_build_user_context_empty(self, adapter_with_mock):
"""测试构建用户上下文(空信息)"""
result = adapter_with_mock._build_user_context({})
assert result == "用户信息: 这是私人对话"
def test_set_and_get_agent(self, adapter_with_mock, mock_agent):
"""测试设置和获取Agent(per-chat模式)"""
chat_id = 'test_chat_123'
new_agent = Mock()
# set_agent已废弃
adapter_with_mock.set_agent(new_agent)
# 测试获取特定chat的Agent
result = adapter_with_mock.get_agent(chat_id)
assert result is not None
def test_initialization_with_default_agent(self, adapter_without_agent):
"""测试使用默认Agent初始化"""
chat_id = 'test_chat'
agent = adapter_without_agent.get_agent(chat_id)
assert agent is not None
@pytest.mark.asyncio
async def test_context_handling_edge_cases(self, adapter_with_mock, mock_agent):
"""测试上下文处理边缘情况"""
user_info = {'chat_id': 'test_chat'}
result = await adapter_with_mock.process_message("Test", [], user_info)
mock_agent.process_user_input.assert_called()
def test_user_info_edge_cases(self, adapter_with_mock):
"""测试用户信息边缘情况"""
user_info_with_none = {
'username': None,
'first_name': 'Test',
'last_name': None,
'language_code': '',
'chat_type': 'private'
}
result = adapter_with_mock._build_user_context(user_info_with_none)
assert "名字: Test" in result
class TestClaudeAgentAdapterCoverage:
"""Claude Agent适配器覆盖率提升测试 - V2.2"""
@pytest.fixture
def adapter_with_mocks(self):
"""创建带Mock的适配器"""
with patch('claude_agent.telegram.claude_adapter.PersistenceManager') as mock_pm:
mock_persistence = Mock()
mock_persistence.load_agent_state = Mock(return_value=None)
mock_persistence.save_agent_state = Mock(return_value=True)
mock_persistence.save_conversation_history = Mock()
mock_persistence.load_conversation_history = Mock(return_value=[])
mock_persistence.get_all_chat_ids = Mock(return_value=[])
mock_persistence.cleanup_old_data = Mock(return_value=5)
mock_persistence.get_storage_stats = Mock(return_value={'total_conversations': 10})
mock_pm.return_value = mock_persistence
adapter = ClaudeAgentAdapter(storage_dir="temp/test", bot_id="test_bot")
adapter.persistence = mock_persistence
return adapter
@pytest.fixture
def mock_agent(self):
"""创建Mock Agent"""
agent = Mock()
agent.conversation_history = []
agent.process_user_input = AsyncMock(return_value="Mock response")
agent.to_dict = Mock(return_value={'test': 'data'})
agent.clean_conversation_history = Mock(return_value=0)
agent.trim_conversation_history = Mock(return_value=0)
agent.set_thinking_mode = Mock()
async def mock_streaming():
yield "Mock"
yield " response"
yield " stream"
agent.create_streaming_response = AsyncMock(side_effect=lambda *args: mock_streaming())
return agent
def test_build_user_display_name_variations(self, adapter_with_mocks):
"""测试构建用户显示名的各种情况"""
# 有用户名的情况
user_info1 = {'username': 'testuser', 'first_name': 'Test'}
result1 = adapter_with_mocks._build_user_display_name(123, user_info1)
assert result1 == "@testuser"
# 有完整姓名的情况
user_info2 = {'first_name': 'Test', 'last_name': 'User'}
result2 = adapter_with_mocks._build_user_display_name(123, user_info2)
assert result2 == "Test User"
# 只有first_name的情况
user_info3 = {'first_name': 'Test'}
result3 = adapter_with_mocks._build_user_display_name(123, user_info3)
assert result3 == "Test"
# 回退到ID的情况
user_info4 = {}
result4 = adapter_with_mocks._build_user_display_name(123, user_info4)
assert result4 == "User123"
def test_load_claude_md_scenarios(self, adapter_with_mocks):
"""测试加载CLAUDE.md的各种场景"""
content = adapter_with_mocks._claude_md_content
assert "你是一个有用的AI助手" in content
def test_get_telegram_fallback_response_types(self, adapter_with_mocks):
"""测试不同类型的错误回退响应"""
user_input = "Test message"
# 基础错误
response1 = adapter_with_mocks._get_telegram_fallback_response(user_input, "Processing failed")
assert "主人" in response1
# API错误
response2 = adapter_with_mocks._get_telegram_fallback_response(user_input, "403")
assert "主人" in response2
# 超时错误
response3 = adapter_with_mocks._get_telegram_fallback_response(user_input, "timeout")
assert "主人" in response3
@pytest.mark.asyncio
async def test_save_message_to_agent_functionality(self, adapter_with_mocks, mock_agent):
"""测试保存消息到Agent功能"""
chat_id = "test_chat"
user_id = 123
message = "Test message"
is_bot = False
user_info = {'username': 'testuser', 'first_name': 'Test'}
with patch.object(adapter_with_mocks, '_get_or_create_agent', return_value=mock_agent):
await adapter_with_mocks._save_message_to_agent(chat_id, user_id, message, is_bot, user_info)
adapter_with_mocks._get_or_create_agent.assert_called_once_with(chat_id)
def test_restore_agents_functionality(self, adapter_with_mocks):
"""测试恢复Agent功能"""
adapter_with_mocks._agents.clear()
saved_states = {
"chat_123": {"conversation_history": [{"role": "user", "content": "Hello"}]},
"chat_456": {"conversation_history": [{"role": "assistant", "content": "Hi"}]}
}
adapter_with_mocks.persistence.load_agent_state = Mock(
side_effect=lambda chat_id: saved_states.get(chat_id)
)
adapter_with_mocks.persistence.get_all_chat_ids = Mock(
return_value=list(saved_states.keys())
)
mock_agent_instance = Mock()
mock_agent_instance.clean_conversation_history = Mock(return_value=0)
mock_agent_instance.trim_conversation_history = Mock(return_value=0)
with patch('claude_agent.telegram.claude_adapter.AgentCore') as mock_agent_class:
mock_agent_class.from_dict.return_value = mock_agent_instance
agent1 = adapter_with_mocks._get_or_create_agent("chat_123")
agent2 = adapter_with_mocks._get_or_create_agent("chat_456")
assert len(adapter_with_mocks._agents) == 2
def test_get_storage_stats_functionality(self, adapter_with_mocks):
"""测试获取存储统计功能"""
for i in range(3):
chat_id = f"stats_test_{i}"
adapter_with_mocks._agents[chat_id] = Mock()
stats = adapter_with_mocks.get_storage_stats()
assert "active_agents" in stats
@pytest.mark.asyncio
async def test_cleanup_old_conversations_functionality(self, adapter_with_mocks):
"""测试清理旧对话功能"""
adapter_with_mocks.persistence.cleanup_old_data = Mock(return_value=5)
result = await adapter_with_mocks.cleanup_old_conversations(30)
assert result == 5
class TestClaudeAgentAdapterPersistence:
"""Claude Agent适配器持久化测试 - V2.2"""
@pytest.fixture
def temp_storage_dir(self):
"""创建临时存储目录"""
temp_dir = tempfile.mkdtemp()
yield temp_dir
shutil.rmtree(temp_dir)
@pytest.fixture
def adapter_with_persistence(self, temp_storage_dir):
"""创建带持久化的适配器"""
return ClaudeAgentAdapter(storage_dir=temp_storage_dir)
@pytest.fixture
def sample_context(self):
"""示例对话上下文"""
return [
{'message': 'Hello', 'is_bot': False, 'timestamp': '2024-01-01T10:00:00'},
{'message': 'Hi! How can I help?', 'is_bot': True, 'timestamp': '2024-01-01T10:00:01'},
]
@pytest.fixture
def sample_user_info(self):
"""示例用户信息"""
return {
'username': 'testuser',
'first_name': 'Test',
'last_name': 'User',
'language_code': 'en',
'chat_type': 'private',
'chat_id': 'test_chat_persistence'
}
def test_adapter_initialization_with_persistence(self, temp_storage_dir):
"""测试带持久化的适配器初始化"""
adapter = ClaudeAgentAdapter(storage_dir=temp_storage_dir)
assert adapter.persistence is not None
assert adapter.persistence.storage_dir == Path(temp_storage_dir)
assert hasattr(adapter, '_agents')
@pytest.mark.asyncio
async def test_streaming_response_persistence(self, adapter_with_persistence, sample_context, sample_user_info):
"""测试流式响应的持久化"""
async def mock_streaming_generator():
yield "First chunk"
yield "Second chunk"
yield "Final chunk"
with patch.object(adapter_with_persistence, '_get_or_create_agent') as mock_get_agent:
mock_agent = Mock()
mock_agent.conversation_history = []
mock_agent.to_dict.return_value = {'test': 'data'}
mock_agent.create_streaming_response.return_value = mock_streaming_generator()
mock_get_agent.return_value = mock_agent
with patch.object(adapter_with_persistence.persistence, 'save_agent_state', return_value=True):
with patch.object(adapter_with_persistence.persistence, 'save_conversation_history', return_value=True):
chunks = []
async for chunk in adapter_with_persistence.create_streaming_response(
"Test message", sample_context, sample_user_info
):
chunks.append(chunk)
assert len(chunks) == 3
class TestClaudeAdapterErrorHandling:
"""测试错误处理场景 - V2.2"""
@pytest.fixture
def basic_adapter(self):
"""创建基础适配器"""
with patch('claude_agent.telegram.claude_adapter.PersistenceManager') as mock_pm:
mock_pm.return_value.get_all_chat_ids.return_value = []
return ClaudeAgentAdapter()
@pytest.fixture
def sample_user_info(self):
"""样本用户信息"""
return {
'user_id': 123456789,
'username': 'testuser',
'first_name': 'Test',
'last_name': 'User',
'chat_id': 'test_chat_error'
}
@pytest.fixture
def sample_context(self):
"""样本上下文"""
return [
{
'user_id': 123456789,
'username': 'testuser',
'message': 'Hello',
'timestamp': '2023-01-01T00:00:00'
}
]
def test_get_agent_with_specific_chat_id(self, basic_adapter):
"""测试获取特定聊天ID的Agent"""
chat_id = "test_chat_specific"
with patch.object(basic_adapter, '_get_or_create_agent') as mock_create:
mock_agent = Mock()
mock_create.return_value = mock_agent
basic_adapter._agents[chat_id] = mock_agent
result = basic_adapter.get_agent(chat_id)
assert result == mock_agent
def test_get_agent_with_no_chat_id_and_no_default(self, basic_adapter):
"""测试获取Agent时没有聊天ID且没有默认Agent"""
result = basic_adapter.get_agent()
assert result is None
@pytest.mark.asyncio
async def test_cleanup_old_conversations_logic(self, basic_adapter):
"""测试清理旧对话的逻辑"""
with patch.object(basic_adapter.persistence, 'cleanup_old_data', return_value=5):
result = await basic_adapter.cleanup_old_conversations(days_threshold=30)
assert result == 5
def test_get_storage_stats_details(self, basic_adapter):
"""测试获取存储统计详情"""
mock_stats = {
'agents_count': 10,
'total_conversations': 100,
'storage_size_mb': 25.5
}
with patch.object(basic_adapter.persistence, 'get_storage_stats', return_value=mock_stats):
result = basic_adapter.get_storage_stats()
assert 'active_agents' in result
class TestClaudeAdapterDeprecatedMethods:
"""测试已弃用方法的覆盖率 - V2.2"""
@pytest.fixture
def adapter_with_deprecated(self):
"""创建包含已弃用方法的适配器"""
with patch('claude_agent.telegram.claude_adapter.PersistenceManager') as mock_pm:
mock_pm.return_value.get_all_chat_ids.return_value = []
return ClaudeAgentAdapter()
def test_set_agent_deprecated(self, adapter_with_deprecated):
"""测试已弃用的set_agent方法"""
mock_agent = Mock()
adapter_with_deprecated.set_agent(mock_agent)
# 验证不会崩溃
assert True
class TestClaudeAdapterUtilityMethods:
"""测试实用工具方法 - V2.2"""
@pytest.fixture
def utility_adapter(self):
"""创建用于测试实用方法的适配器"""
with patch('claude_agent.telegram.claude_adapter.PersistenceManager') as mock_pm:
mock_pm.return_value.get_all_chat_ids.return_value = []
return ClaudeAgentAdapter()
def test_build_user_display_name_variations(self, utility_adapter):
"""测试用户显示名的各种变化"""
# 有用户名的情况
user_info_with_username = {
'user_id': 123,
'username': 'testuser',
'first_name': 'Test',
'last_name': 'User'
}
display_name = utility_adapter._build_user_display_name(123, user_info_with_username)
assert display_name == "@testuser"
# 没有用户名但有姓名的情况
user_info_with_names = {
'user_id': 123,
'first_name': 'Test',
'last_name': 'User'
}
display_name = utility_adapter._build_user_display_name(123, user_info_with_names)
assert display_name == "Test User"
# 只有first_name的情况
user_info_first_only = {
'user_id': 123,
'first_name': 'Test'
}
display_name = utility_adapter._build_user_display_name(123, user_info_first_only)
assert display_name == "Test"
# 什么都没有的情况
user_info_minimal = {'user_id': 123}
display_name = utility_adapter._build_user_display_name(123, user_info_minimal)
assert display_name == "User123"
def test_build_user_context_comprehensive(self, utility_adapter):
"""测试构建用户上下文的综合情况"""
user_info = {
'user_id': 123456789,
'username': 'testuser',
'first_name': 'Test',
'last_name': 'User',
'chat_id': 'test_chat'
}
context = utility_adapter._build_user_context(user_info)
assert "用户名: @testuser" in context
assert "名字: Test" in context
assert "姓氏: User" in context
def test_get_telegram_fallback_response_quota_error(self, utility_adapter):
"""测试Telegram回退响应的配额错误"""
result = utility_adapter._get_telegram_fallback_response(
"Test input", "预扣费额度失败"
)
assert "AI大脑暂时欠费了" in result
def test_get_telegram_fallback_response_403_error(self, utility_adapter):
"""测试Telegram回退响应的403错误"""
result = utility_adapter._get_telegram_fallback_response(
"Test input", "403"
)
assert "AI大脑暂时欠费了" in result
def test_get_telegram_fallback_response_timeout_error(self, utility_adapter):
"""测试Telegram回退响应的超时错误"""
result = utility_adapter._get_telegram_fallback_response(
"Test input", "timeout"
)
assert "大脑好像卡住了" in result or "卡住了一下下" in result