blob: 758c66874605e0a679d36e28475f42e373d6ec4e [file] [log] [blame] [raw]
"""
SSHOUT模块简化测试
只测试基本导入和简单功能
"""
import pytest
from unittest.mock import Mock, patch, AsyncMock, MagicMock
import asyncio
from datetime import datetime
import struct
import paramiko
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
class TestSSHOUTModuleBasics:
"""SSHOUT模块基础测试"""
def test_module_imports(self):
"""测试模块能正常导入"""
try:
import claude_agent.sshout as sshout_module
assert sshout_module is not None
except ImportError:
pytest.fail("无法导入SSHOUT模块")
def test_all_exports_exist(self):
"""测试__all__导出的所有项目都存在"""
import claude_agent.sshout as sshout_module
if hasattr(sshout_module, '__all__'):
for export_name in sshout_module.__all__:
assert hasattr(sshout_module, export_name), f"导出项 {export_name} 不存在"
def test_create_integration_function_exists(self):
"""测试create_sshout_integration函数存在"""
from claude_agent.sshout import create_sshout_integration
assert callable(create_sshout_integration)
def test_create_integration_with_valid_config(self):
"""测试使用有效配置创建集成"""
from claude_agent.sshout import create_sshout_integration
mock_agent = Mock()
# 正确的patch路径
with patch('claude_agent.utils.config.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'connection_mode': 'api',
'api_endpoint': 'wss://example.com',
'username': 'testuser',
'password': 'testpass',
'mention_patterns': ['@Claude']
}
mock_get_config.return_value = mock_config_manager
# Mock SSHOUTApiIntegration类
with patch('claude_agent.sshout.SSHOUTApiIntegration') as mock_integration:
mock_instance = Mock()
mock_integration.return_value = mock_instance
result = create_sshout_integration(mock_agent, "test_config")
assert result == mock_instance
mock_integration.assert_called_once_with(mock_agent, "test_config")
def test_create_integration_ssh_mode(self):
"""测试SSH模式创建集成"""
from claude_agent.sshout import create_sshout_integration
mock_agent = Mock()
with patch('claude_agent.utils.config.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'connection_mode': 'ssh',
'server': {
'hostname': 'sshout.example.com',
'port': 22,
'username': 'sshout'
},
'ssh_key': {
'private_key_path': '/path/to/key'
}
}
mock_get_config.return_value = mock_config_manager
with patch('claude_agent.sshout.SSHOUTIntegration') as mock_integration:
mock_instance = Mock()
mock_integration.return_value = mock_instance
result = create_sshout_integration(mock_agent, "test_config")
assert result == mock_instance
mock_integration.assert_called_once_with(mock_agent, "test_config")
def test_create_integration_invalid_mode(self):
"""测试无效连接模式"""
from claude_agent.sshout import create_sshout_integration
mock_agent = Mock()
with patch('claude_agent.utils.config.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'connection_mode': 'invalid_mode'
}
mock_get_config.return_value = mock_config_manager
with pytest.raises(ValueError, match="不支持的SSHOUT连接模式"):
create_sshout_integration(mock_agent)
def test_create_integration_default_api_mode(self):
"""测试默认API模式"""
from claude_agent.sshout import create_sshout_integration
mock_agent = Mock()
with patch('claude_agent.utils.config.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
# 不设置connection_mode,应该默认为api
mock_config_manager.get_sshout_config.return_value = {}
mock_get_config.return_value = mock_config_manager
with patch('claude_agent.sshout.SSHOUTApiIntegration') as mock_integration:
mock_instance = Mock()
mock_integration.return_value = mock_instance
result = create_sshout_integration(mock_agent)
assert result == mock_instance
mock_integration.assert_called_once_with(mock_agent, None)
def test_config_error_handling(self):
"""测试配置错误处理"""
from claude_agent.sshout import create_sshout_integration
mock_agent = Mock()
with patch('claude_agent.utils.config.get_config_manager') as mock_get_config:
# 模拟配置获取失败
mock_get_config.side_effect = Exception("配置文件错误")
with pytest.raises(Exception):
create_sshout_integration(mock_agent)
class TestSSHOUTImportStructure:
"""测试SSHOUT导入结构"""
def test_integration_classes_importable(self):
"""测试集成类可以导入"""
try:
from claude_agent.sshout.integration import SSHOUTIntegration, SSHOUTConnection, SSHOUTMessage
assert SSHOUTIntegration is not None
assert SSHOUTConnection is not None
assert SSHOUTMessage is not None
except ImportError as e:
# 如果导入失败,至少确保我们知道原因
assert "SSHOUTIntegration" in str(e) or "SSHOUTConnection" in str(e) or "SSHOUTMessage" in str(e)
def test_api_client_classes_importable(self):
"""测试API客户端类可以导入"""
try:
from claude_agent.sshout.api_client import SSHOUTApiClient, SSHOUTApiIntegration
assert SSHOUTApiClient is not None
assert SSHOUTApiIntegration is not None
except ImportError as e:
# 如果导入失败,至少确保我们知道原因
assert "SSHOUTApiClient" in str(e) or "SSHOUTApiIntegration" in str(e)
def test_sshout_module_structure(self):
"""测试SSHOUT模块结构"""
import claude_agent.sshout as sshout
# 检查模块是否有__all__属性
if hasattr(sshout, '__all__'):
assert isinstance(sshout.__all__, list)
assert len(sshout.__all__) > 0
# 检查create_sshout_integration函数
assert hasattr(sshout, 'create_sshout_integration')
assert callable(sshout.create_sshout_integration)
def test_docstring_exists(self):
"""测试模块文档字符串存在"""
import claude_agent.sshout as sshout
assert sshout.__doc__ is not None
assert len(sshout.__doc__.strip()) > 0
class TestSSHOUTBasicFunctionality:
"""测试SSHOUT基础功能"""
def test_function_signature(self):
"""测试create_sshout_integration函数签名"""
from claude_agent.sshout import create_sshout_integration
import inspect
sig = inspect.signature(create_sshout_integration)
params = list(sig.parameters.keys())
assert 'agent_core' in params
assert 'config_name' in params
# 检查config_name是否有默认值
config_param = sig.parameters['config_name']
assert config_param.default is None
def test_error_messages(self):
"""测试错误消息格式"""
from claude_agent.sshout import create_sshout_integration
mock_agent = Mock()
with patch('claude_agent.utils.config.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'connection_mode': 'unsupported_mode'
}
mock_get_config.return_value = mock_config_manager
with pytest.raises(ValueError) as exc_info:
create_sshout_integration(mock_agent)
error_message = str(exc_info.value)
assert "不支持的SSHOUT连接模式" in error_message
assert "unsupported_mode" in error_message
class TestSSHOUTApiClientBasics:
"""SSHOUT API Client基础测试"""
def test_api_client_initialization(self):
"""测试API客户端初始化"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key",
timeout=10
)
assert client.hostname == "test.example.com"
assert client.port == 22
assert client.username == "testuser"
assert client.key_path == "/path/to/key"
assert client.timeout == 10
assert client.connected is False
assert client.client is None
assert client.channel is None
assert client.message_history == []
assert client.max_history == 100
def test_api_client_initialization_with_mention_patterns(self):
"""测试API客户端初始化(带mention模式)"""
from claude_agent.sshout.api_client import SSHOUTApiClient
custom_patterns = ["@TestBot", "TestBot:", "TestBot,"]
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key",
mention_patterns=custom_patterns
)
assert client.mention_patterns == custom_patterns
def test_api_client_default_mention_patterns(self):
"""测试API客户端默认mention模式"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
expected_patterns = [
"@Claude", "@claude", "@CLAUDE",
"Claude:", "claude:",
"Claude,", "claude,",
"Claude,", "claude,"
]
assert client.mention_patterns == expected_patterns
def test_sshout_message_dataclass(self):
"""测试SSHOUT消息数据结构"""
from claude_agent.sshout.api_client import SSHOUTMessage, SSHOUTMessageType
timestamp = datetime.now()
message = SSHOUTMessage(
timestamp=timestamp,
from_user="user1",
to_user="user2",
message_type=SSHOUTMessageType.PLAIN,
content="Hello world",
is_mention=True
)
assert message.timestamp == timestamp
assert message.from_user == "user1"
assert message.to_user == "user2"
assert message.message_type == SSHOUTMessageType.PLAIN
assert message.content == "Hello world"
assert message.is_mention is True
def test_sshout_user_dataclass(self):
"""测试SSHOUT用户数据结构"""
from claude_agent.sshout.api_client import SSHOUTUser
user = SSHOUTUser(
id=123,
username="testuser",
hostname="client.example.com"
)
assert user.id == 123
assert user.username == "testuser"
assert user.hostname == "client.example.com"
def test_packet_type_enum_values(self):
"""测试包类型枚举值"""
from claude_agent.sshout.api_client import SSHOUTPacketType
assert SSHOUTPacketType.HELLO == 1
assert SSHOUTPacketType.GET_ONLINE_USER == 2
assert SSHOUTPacketType.SEND_MESSAGE == 3
assert SSHOUTPacketType.GET_MOTD == 4
assert SSHOUTPacketType.PASS == 128
assert SSHOUTPacketType.ONLINE_USERS_INFO == 129
assert SSHOUTPacketType.RECEIVE_MESSAGE == 130
assert SSHOUTPacketType.USER_STATE_CHANGE == 131
assert SSHOUTPacketType.ERROR == 132
assert SSHOUTPacketType.MOTD == 133
def test_message_type_enum_values(self):
"""测试消息类型枚举值"""
from claude_agent.sshout.api_client import SSHOUTMessageType
assert SSHOUTMessageType.PLAIN == 1
assert SSHOUTMessageType.RICH == 2
assert SSHOUTMessageType.IMAGE == 3
def test_error_code_enum_values(self):
"""测试错误码枚举值"""
from claude_agent.sshout.api_client import SSHOUTErrorCode
assert SSHOUTErrorCode.SERVER_CLOSED == 1
assert SSHOUTErrorCode.LOCAL_PACKET_CORRUPT == 2
assert SSHOUTErrorCode.LOCAL_PACKET_TOO_LARGE == 3
assert SSHOUTErrorCode.OUT_OF_MEMORY == 4
assert SSHOUTErrorCode.INTERNAL_ERROR == 5
assert SSHOUTErrorCode.USER_NOT_FOUND == 6
assert SSHOUTErrorCode.MOTD_NOT_AVAILABLE == 7
def test_add_message_callback(self):
"""测试添加消息回调"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
callback = Mock()
client.add_message_callback(callback)
assert callback in client.message_callbacks
def test_add_mention_callback(self):
"""测试添加mention回调"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
callback = Mock()
client.add_mention_callback(callback)
assert callback in client.mention_callbacks
def test_get_recent_messages_empty(self):
"""测试获取最近消息(空列表)"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
messages = client.get_recent_messages(5)
assert messages == []
def test_get_recent_messages_with_data(self):
"""测试获取最近消息(有数据)"""
from claude_agent.sshout.api_client import SSHOUTApiClient, SSHOUTMessage, SSHOUTMessageType
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 添加一些消息到历史
for i in range(10):
message = SSHOUTMessage(
timestamp=datetime.now(),
from_user=f"user{i}",
to_user="public",
message_type=SSHOUTMessageType.PLAIN,
content=f"Message {i}"
)
client.message_history.append(message)
# 获取最近5条消息
recent = client.get_recent_messages(5)
assert len(recent) == 5
assert recent[0].content == "Message 5" # 最早的
assert recent[4].content == "Message 9" # 最新的
def test_get_recent_messages_limit_exceeds(self):
"""测试获取最近消息(超出历史数量)"""
from claude_agent.sshout.api_client import SSHOUTApiClient, SSHOUTMessage, SSHOUTMessageType
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 只添加3条消息
for i in range(3):
message = SSHOUTMessage(
timestamp=datetime.now(),
from_user=f"user{i}",
to_user="public",
message_type=SSHOUTMessageType.PLAIN,
content=f"Message {i}"
)
client.message_history.append(message)
# 请求5条消息,应该返回所有3条
recent = client.get_recent_messages(5)
assert len(recent) == 3
def test_detect_mention_true(self):
"""测试mention检测(匹配)"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
test_cases = [
"@Claude hello",
"@claude what's up",
"Claude: can you help?",
"claude, please assist",
"Hey Claude, how are you?"
]
for content in test_cases:
assert client._is_claude_mention(content) is True, f"Failed to detect mention in: {content}"
def test_detect_mention_false(self):
"""测试mention检测(不匹配)"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
test_cases = [
"Hello everyone",
"This is a regular message",
"Claude3 is nice", # 不是完整的mention
"Claudette is here", # 不是Claude
"Someone named Claude" # 不是直接mention格式
]
for content in test_cases:
assert client._is_claude_mention(content) is False, f"False positive mention in: {content}"
def test_detect_mention_custom_patterns(self):
"""测试自定义mention模式检测"""
from claude_agent.sshout.api_client import SSHOUTApiClient
custom_patterns = ["@Bot", "Bot:", "Hey Bot"]
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key",
mention_patterns=custom_patterns
)
assert client._is_claude_mention("@Bot help me") is True
assert client._is_claude_mention("Bot: what time is it?") is True
assert client._is_claude_mention("Hey Bot can you assist?") is True
assert client._is_claude_mention("@Claude hello") is False # 默认模式不再有效
def test_trim_message_history(self):
"""测试修剪消息历史"""
from claude_agent.sshout.api_client import SSHOUTApiClient, SSHOUTMessage, SSHOUTMessageType
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
client.max_history = 5 # 设置小的历史限制
# 添加10条消息
for i in range(10):
message = SSHOUTMessage(
timestamp=datetime.now(),
from_user=f"user{i}",
to_user="public",
message_type=SSHOUTMessageType.PLAIN,
content=f"Message {i}"
)
client.message_history.append(message)
# 手动触发历史修剪 (实际实现中是自动的)
if len(client.message_history) > client.max_history:
client.message_history = client.message_history[-client.max_history:]
# 应该只保留最近的5条消息
assert len(client.message_history) == 5
assert client.message_history[0].content == "Message 5"
assert client.message_history[4].content == "Message 9"
def test_connection_status(self):
"""测试连接状态检查"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 测试未连接状态
status = client.get_connection_status()
assert status['connected'] is False
assert status['server'] is None
assert status['message_count'] == 0
# 测试已连接状态
client.connected = True
client.my_user_id = 123
client.my_username = "testuser"
status = client.get_connection_status()
assert status['connected'] is True
assert status['server'] == "test.example.com:22"
assert status['my_user_id'] == 123
assert status['my_username'] == "testuser"
class TestSSHOUTApiClientMessageMethods:
"""SSHOUT API Client消息方法测试"""
@pytest.mark.asyncio
async def test_send_message_not_connected(self):
"""测试未连接时发送消息"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
result = await client.send_message("public", "Hello world")
assert result is False
@pytest.mark.asyncio
async def test_send_global_message_not_connected(self):
"""测试未连接时发送全局消息"""
from claude_agent.sshout.api_client import SSHOUTApiClient
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
result = await client.send_global_message("Hello everyone")
assert result is False
def test_get_context_messages(self):
"""测试获取上下文消息"""
from claude_agent.sshout.api_client import SSHOUTApiClient, SSHOUTMessage, SSHOUTMessageType
client = SSHOUTApiClient(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 添加一些历史消息
base_time = datetime.now()
for i in range(10):
message_time = base_time.replace(second=i)
message = SSHOUTMessage(
timestamp=message_time,
from_user=f"user{i}",
to_user="public",
message_type=SSHOUTMessageType.PLAIN,
content=f"Message {i}"
)
client.message_history.append(message)
# 获取第5条消息之前的上下文
target_time = base_time.replace(second=5)
context = client.get_context_messages(target_time, count=3)
assert len(context) == 3
assert context[0].content == "Message 2" # 最早的上下文
assert context[2].content == "Message 4" # 最近的上下文(时间小于target_time)
class TestSSHOUTApiIntegrationBasics:
"""SSHOUT API Integration基础测试"""
@pytest.fixture
def mock_agent(self):
"""创建模拟Agent"""
return Mock()
def test_integration_initialization_missing_config(self, mock_agent):
"""测试集成初始化(缺少配置)"""
from claude_agent.sshout.api_client import SSHOUTApiIntegration
with patch('claude_agent.sshout.api_client.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {} # 空配置
mock_get_config.return_value = mock_config_manager
with pytest.raises(ValueError, match="SSHOUT配置缺少必需的段落"):
SSHOUTApiIntegration(mock_agent)
def test_integration_initialization_missing_key_file(self, mock_agent):
"""测试集成初始化(私钥文件不存在)"""
from claude_agent.sshout.api_client import SSHOUTApiIntegration
with patch('claude_agent.sshout.api_client.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {
'hostname': 'test.example.com',
'port': 22,
'username': 'testuser'
},
'ssh_key': {
'private_key_path': '/nonexistent/key'
}
}
mock_get_config.return_value = mock_config_manager
with pytest.raises(FileNotFoundError, match="SSH私钥文件不存在"):
SSHOUTApiIntegration(mock_agent)
@pytest.mark.asyncio
async def test_disconnect_from_sshout_api(self, mock_agent):
"""测试断开SSHOUT API连接"""
from claude_agent.sshout.api_client import SSHOUTApiIntegration
with patch('claude_agent.sshout.api_client.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.api_client.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTApiIntegration(mock_agent)
# 设置模拟客户端
mock_client = AsyncMock()
integration.client = mock_client
await integration.disconnect_from_sshout_api()
mock_client.disconnect.assert_called_once()
assert integration.client is None
def test_get_connection_status_no_client(self, mock_agent):
"""测试获取连接状态(无客户端)"""
from claude_agent.sshout.api_client import SSHOUTApiIntegration
with patch('claude_agent.sshout.api_client.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.api_client.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTApiIntegration(mock_agent)
status = integration.get_connection_status()
assert status['connected'] is False
assert status['api_version'] == '1.0'
@pytest.mark.asyncio
async def test_send_message_no_client(self, mock_agent):
"""测试发送消息(无客户端)"""
from claude_agent.sshout.api_client import SSHOUTApiIntegration
with patch('claude_agent.sshout.api_client.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.api_client.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTApiIntegration(mock_agent)
result = await integration.send_message("Test message")
assert result is False
def test_clean_response_for_sshout(self, mock_agent):
"""测试清理响应文本"""
from claude_agent.sshout.api_client import SSHOUTApiIntegration
with patch('claude_agent.sshout.api_client.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.api_client.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_config_manager.get.return_value = 0 # 无长度限制
mock_get_config.return_value = mock_config_manager
integration = SSHOUTApiIntegration(mock_agent)
# 测试清理markdown格式
text = "这是**粗体**和*斜体*以及`代码`文本"
cleaned = integration._clean_response_for_sshout(text)
assert cleaned == "这是粗体和斜体以及代码文本"
# 测试清理多余换行
text = "行1\n\n\n\n行2"
cleaned = integration._clean_response_for_sshout(text)
assert cleaned == "行1\n\n行2"
class TestSSHOUTConnectionBasics:
"""SSHOUT Connection基础测试"""
def test_connection_initialization(self):
"""测试连接初始化"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
assert connection.hostname == "test.example.com"
assert connection.port == 22
assert connection.username == "testuser"
assert connection.key_path == "/path/to/key"
assert connection.connected is False
assert connection.client is None
assert connection.shell is None
assert len(connection.message_callbacks) == 0
assert len(connection.mention_callbacks) == 0
assert len(connection.message_history) == 0
assert connection.max_history == 100
def test_connection_initialization_with_custom_patterns(self):
"""测试带自定义mention模式的连接初始化"""
from claude_agent.sshout.integration import SSHOUTConnection
custom_patterns = ["@Bot", "Bot:", "Hey Bot"]
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key",
mention_patterns=custom_patterns
)
assert connection.mention_patterns == custom_patterns
def test_connection_default_mention_patterns(self):
"""测试默认mention模式"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
expected_patterns = [
"@Claude", "@claude", "@CLAUDE",
"Claude:", "claude:",
"Claude,", "claude,",
"Claude,", "claude,"
]
assert connection.mention_patterns == expected_patterns
def test_add_message_callback(self):
"""测试添加消息回调"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
callback = Mock()
connection.add_message_callback(callback)
assert callback in connection.message_callbacks
def test_add_mention_callback(self):
"""测试添加mention回调"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
callback = Mock()
connection.add_mention_callback(callback)
assert callback in connection.mention_callbacks
def test_get_recent_messages_empty(self):
"""测试获取最近消息(空列表)"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
messages = connection.get_recent_messages(5)
assert messages == []
def test_get_recent_messages_with_data(self):
"""测试获取最近消息(有数据)"""
from claude_agent.sshout.integration import SSHOUTConnection, SSHOUTMessage
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 添加一些消息到历史
for i in range(10):
message = SSHOUTMessage(
timestamp=datetime.now(),
username=f"user{i}",
content=f"Message {i}"
)
connection.message_history.append(message)
# 获取最近5条消息
recent = connection.get_recent_messages(5)
assert len(recent) == 5
assert recent[0].content == "Message 5" # 最早的
assert recent[4].content == "Message 9" # 最新的
def test_get_context_messages(self):
"""测试获取上下文消息"""
from claude_agent.sshout.integration import SSHOUTConnection, SSHOUTMessage
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 添加一些历史消息
base_time = datetime.now()
for i in range(10):
message_time = base_time.replace(second=i)
message = SSHOUTMessage(
timestamp=message_time,
username=f"user{i}",
content=f"Message {i}"
)
connection.message_history.append(message)
# 获取第5条消息之前的上下文
target_time = base_time.replace(second=5)
context = connection.get_context_messages(target_time, count=3)
assert len(context) == 3
assert context[0].content == "Message 2" # 最早的上下文
assert context[2].content == "Message 4" # 最近的上下文(时间小于target_time)
def test_is_claude_mention_detection(self):
"""测试@Claude提及检测"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 测试匹配的情况
test_cases_true = [
"@Claude hello",
"@claude what's up",
"Claude: can you help?",
"claude, please assist",
"Hey Claude, how are you?"
]
for content in test_cases_true:
assert connection._is_claude_mention(content) is True, f"Failed to detect mention in: {content}"
# 测试不匹配的情况
test_cases_false = [
"Hello everyone",
"This is a regular message",
"Claude3 is nice", # 不是完整的mention
"Claudette is here", # 不是Claude
"Someone named Claude" # 不是直接mention格式
]
for content in test_cases_false:
assert connection._is_claude_mention(content) is False, f"False positive mention in: {content}"
def test_is_claude_mention_custom_patterns(self):
"""测试自定义mention模式检测"""
from claude_agent.sshout.integration import SSHOUTConnection
custom_patterns = ["@Bot", "Bot:", "Hey Bot"]
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key",
mention_patterns=custom_patterns
)
assert connection._is_claude_mention("@Bot help me") is True
assert connection._is_claude_mention("Bot: what time is it?") is True
assert connection._is_claude_mention("Hey Bot can you assist?") is True
assert connection._is_claude_mention("@Claude hello") is False # 默认模式不再有效
@pytest.mark.asyncio
async def test_send_message_not_connected(self):
"""测试未连接时发送消息"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
result = await connection.send_message("Hello world")
assert result is False
@pytest.mark.asyncio
async def test_disconnect_cleanup(self):
"""测试断开连接时清理资源"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 设置已连接状态
connection.connected = True
connection.shell = Mock()
connection.client = Mock()
# 执行断开连接
await connection.disconnect()
# 验证状态重置
assert connection.connected is False
assert connection.shell is None
assert connection.client is None
class TestSSHOUTMessageParsing:
"""SSHOUT消息解析测试"""
def test_sshout_message_creation(self):
"""测试SSHOUT消息创建"""
from claude_agent.sshout.integration import SSHOUTMessage
timestamp = datetime.now()
message = SSHOUTMessage(
timestamp=timestamp,
username="testuser",
content="Hello world",
is_mention=True
)
assert message.timestamp == timestamp
assert message.username == "testuser"
assert message.content == "Hello world"
assert message.is_mention is True
def test_clean_ansi_codes(self):
"""测试清理ANSI转义码"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 测试ANSI转义码清理
test_cases = [
("\x1b[1;34mHello\x1b[0m", "Hello"),
("[1;34mWorld[0m", "World"),
("Normal text", "Normal text"),
("\x1b[31mRed\x1b[0m and \x1b[32mGreen\x1b[0m", "Red and Green"),
]
for input_text, expected in test_cases:
result = connection._clean_ansi_codes(input_text)
assert result == expected, f"Expected '{expected}', got '{result}' for input '{input_text}'"
def test_parse_message_various_formats(self):
"""测试解析各种消息格式"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
test_cases = [
# 格式1: [HH:MM:SS] <username> message
"[12:34:56] <alice> Hello everyone",
# 格式2: <username> message
"<bob> How are you?",
# 格式3: username: message
"charlie: Good morning!",
# 格式4: [HH:MM:SS] username: message
"[09:15:30] dave: Testing message",
]
for line in test_cases:
message = connection._parse_message(line)
assert message is not None, f"Failed to parse: {line}"
assert message.username is not None
assert message.content is not None
assert message.timestamp is not None
def test_parse_message_with_ansi_codes(self):
"""测试解析带ANSI转义码的消息"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
# 带ANSI转义码的消息
line = "\x1b[1;34m[12:34:56] <alice> \x1b[0mHello \x1b[31mworld\x1b[0m"
message = connection._parse_message(line)
assert message is not None
assert message.username == "alice"
assert message.content == "Hello world"
def test_parse_message_invalid_format(self):
"""测试解析无效格式的消息"""
from claude_agent.sshout.integration import SSHOUTConnection
connection = SSHOUTConnection(
hostname="test.example.com",
port=22,
username="testuser",
key_path="/path/to/key"
)
invalid_lines = [
"Just some random text",
">>> System message <<<",
"",
" ",
"12:34:56 without brackets or username",
]
for line in invalid_lines:
message = connection._parse_message(line)
# Some of these might return None, which is expected
if message is not None:
# If parsed, should have valid fields
assert message.username is not None
assert message.content is not None
class TestSSHOUTIntegrationAdvanced:
"""SSHOUT Integration高级测试"""
@pytest.fixture
def mock_agent(self):
"""创建模拟Agent"""
return Mock()
def test_integration_initialization_with_valid_config(self, mock_agent):
"""测试集成初始化(有效配置)"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {
'hostname': 'test.example.com',
'port': 22,
'username': 'testuser'
},
'ssh_key': {
'private_key_path': '/path/to/key'
}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
assert integration.agent == mock_agent
assert integration.connection is None
assert integration.sshout_config is not None
def test_integration_initialization_missing_server_config(self, mock_agent):
"""测试集成初始化(缺少服务器配置键)"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {
'hostname': 'test.example.com'
# 缺少 port 和 username
},
'ssh_key': {
'private_key_path': '/path/to/key'
}
}
mock_get_config.return_value = mock_config_manager
with pytest.raises(ValueError, match="SSHOUT服务器配置缺少必需的键"):
SSHOUTIntegration(mock_agent)
def test_integration_initialization_missing_ssh_key(self, mock_agent):
"""测试集成初始化(缺少SSH密钥配置)"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {
'hostname': 'test.example.com',
'port': 22,
'username': 'testuser'
},
'ssh_key': {
# 缺少 private_key_path
}
}
mock_get_config.return_value = mock_config_manager
with pytest.raises(ValueError, match="SSHOUT配置缺少SSH私钥路径"):
SSHOUTIntegration(mock_agent)
@pytest.mark.asyncio
async def test_disconnect_from_sshout(self, mock_agent):
"""测试断开SSHOUT连接"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
# 设置模拟连接
mock_connection = AsyncMock()
integration.connection = mock_connection
await integration.disconnect_from_sshout()
mock_connection.disconnect.assert_called_once()
assert integration.connection is None
def test_on_message_received(self, mock_agent):
"""测试接收普通消息回调"""
from claude_agent.sshout.integration import SSHOUTIntegration, SSHOUTMessage
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
# 创建测试消息
message = SSHOUTMessage(
timestamp=datetime.now(),
username="testuser",
content="Hello world"
)
# 调用回调函数不应该抛出异常
integration._on_message_received(message)
def test_get_connection_status_no_connection(self, mock_agent):
"""测试获取连接状态(无连接)"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
status = integration.get_connection_status()
assert status['connected'] is False
assert status['server'] is None
assert status['message_count'] == 0
def test_get_connection_status_with_connection(self, mock_agent):
"""测试获取连接状态(有连接)"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
# 设置模拟连接
mock_connection = Mock()
mock_connection.connected = True
mock_connection.hostname = "test.com"
mock_connection.port = 22
mock_connection.message_history = ["msg1", "msg2"]
mock_connection.get_recent_messages.return_value = []
integration.connection = mock_connection
status = integration.get_connection_status()
assert status['connected'] is True
assert status['server'] == "test.com:22"
assert status['message_count'] == 2
@pytest.mark.asyncio
async def test_send_message_no_connection(self, mock_agent):
"""测试发送消息(无连接)"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
result = await integration.send_message("Test message")
assert result is False
@pytest.mark.asyncio
async def test_send_message_with_connection(self, mock_agent):
"""测试发送消息(有连接)"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
# 设置模拟连接
mock_connection = AsyncMock()
mock_connection.connected = True
mock_connection.send_message.return_value = True
integration.connection = mock_connection
result = await integration.send_message("Test message")
assert result is True
mock_connection.send_message.assert_called_once_with("Test message")
def test_clean_response_for_sshout_formatting(self, mock_agent):
"""测试清理响应文本格式"""
from claude_agent.sshout.integration import SSHOUTIntegration
with patch('claude_agent.sshout.integration.get_config_manager') as mock_get_config, \
patch('claude_agent.sshout.integration.os.path.exists', return_value=True):
mock_config_manager = Mock()
mock_config_manager.get_sshout_config.return_value = {
'server': {'hostname': 'test.com', 'port': 22, 'username': 'user'},
'ssh_key': {'private_key_path': '/test/key'}
}
mock_config_manager.get.return_value = 0 # 无长度限制
mock_get_config.return_value = mock_config_manager
integration = SSHOUTIntegration(mock_agent)
# 测试清理markdown格式
text = "这是**粗体**和*斜体*以及`代码`文本"
cleaned = integration._clean_response_for_sshout(text)
assert cleaned == "这是粗体和斜体以及代码文本"
# 测试清理多余换行
text = "行1\n\n\n\n行2"
cleaned = integration._clean_response_for_sshout(text)
assert cleaned == "行1\n\n行2"
# 测试长度限制
mock_config_manager.get.return_value = 10 # 设置长度限制
long_text = "这是一个很长的文本内容用于测试长度限制功能"
cleaned = integration._clean_response_for_sshout(long_text)
assert cleaned == "这是一个很长的文本内..."
assert len(cleaned) <= 13 # 10 + "..."