blob: 6c0776303232e52b02ff23e948b3d7afb36d7641 [file] [log] [blame] [raw]
"""
配置管理器单元测试
测试TOML配置文件的加载和管理功能
"""
import unittest
import tempfile
import os
import shutil
from unittest.mock import patch, mock_open
from src.claude_agent.utils.config import ConfigManager, get_config_manager, get_config
class TestConfigManager(unittest.TestCase):
"""ConfigManager类的单元测试"""
def setUp(self):
"""设置测试环境"""
# 创建临时目录作为项目根目录
self.test_dir = tempfile.mkdtemp()
self.config_dir = os.path.join(self.test_dir, "configs")
os.makedirs(self.config_dir, exist_ok=True)
def tearDown(self):
"""清理测试环境"""
shutil.rmtree(self.test_dir, ignore_errors=True)
def _create_test_config(self, filename: str, content: str):
"""创建测试配置文件"""
config_path = os.path.join(self.config_dir, filename)
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
return config_path
def _patch_project_root(self, config_manager):
"""修改ConfigManager的项目根目录为测试目录"""
config_manager._project_root = self.test_dir
config_manager._config_dir = self.config_dir
def test_valid_toml_config_loading(self):
"""测试有效TOML配置文件的加载"""
# 创建有效的TOML配置
toml_content = """
[agent]
default_mode = "interactive"
max_conversation_history = 100
[sshout]
mention_patterns = ["@Claude", "@claude", "Claude:"]
[sshout.server]
hostname = "test.server.com"
port = 22333
username = "testuser"
[sshout.ssh_key]
private_key_path = "test/path/to/key"
timeout = 10
[sshout.message]
max_history = 50
context_count = 3
max_reply_length = 150
[logging]
level = "INFO"
colored = true
"""
self._create_test_config("test.toml", toml_content)
# 测试配置加载
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("test")
# 验证基本配置加载
self.assertEqual(config_manager.get("agent.default_mode"), "interactive")
self.assertEqual(config_manager.get("agent.max_conversation_history"), 100)
# 验证嵌套配置加载
self.assertEqual(config_manager.get("sshout.server.hostname"), "test.server.com")
self.assertEqual(config_manager.get("sshout.server.port"), 22333)
self.assertEqual(config_manager.get("sshout.ssh_key.timeout"), 10)
# 验证数组配置
patterns = config_manager.get("sshout.mention_patterns")
self.assertIsInstance(patterns, list)
self.assertIn("@Claude", patterns)
def test_missing_config_file(self):
"""测试配置文件不存在的情况"""
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
with self.assertRaises(FileNotFoundError) as context:
ConfigManager("nonexistent")
self.assertIn("配置文件不存在", str(context.exception))
def test_invalid_toml_format(self):
"""测试无效TOML格式的配置文件"""
# 创建语法错误的TOML文件
invalid_toml = """
[agent
default_mode = "interactive" # 缺少闭括号
[sshout]
invalid = toml = syntax
"""
self._create_test_config("invalid.toml", invalid_toml)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
with self.assertRaises(ValueError) as context:
ConfigManager("invalid")
self.assertIn("配置文件格式错误", str(context.exception))
def test_get_method_with_default_values(self):
"""测试get方法的默认值处理"""
toml_content = """
[existing]
value = "test"
"""
self._create_test_config("defaults.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("defaults")
# 测试存在的键
self.assertEqual(config_manager.get("existing.value"), "test")
# 测试不存在的键,使用默认值
self.assertEqual(config_manager.get("nonexistent.key", "default"), "default")
self.assertIsNone(config_manager.get("nonexistent.key"))
def test_get_section_method(self):
"""测试get_section方法"""
toml_content = """
[section1]
key1 = "value1"
key2 = 42
[section2]
[section2.subsection]
nested = true
"""
self._create_test_config("sections.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("sections")
# 测试获取存在的段落
section1 = config_manager.get_section("section1")
self.assertIsInstance(section1, dict)
self.assertEqual(section1["key1"], "value1")
self.assertEqual(section1["key2"], 42)
# 测试获取嵌套段落
section2 = config_manager.get_section("section2")
self.assertIn("subsection", section2)
self.assertTrue(section2["subsection"]["nested"])
# 测试获取不存在的段落
empty_section = config_manager.get_section("nonexistent")
self.assertEqual(empty_section, {})
def test_sshout_config_method(self):
"""测试get_sshout_config方法和路径处理"""
toml_content = """
[sshout]
[sshout.server]
hostname = "sshout.test.com"
port = 22333
username = "sshout_user"
[sshout.ssh_key]
private_key_path = "relative/path/to/key"
timeout = 15
[sshout.message]
max_history = 200
"""
self._create_test_config("sshout.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("sshout")
sshout_config = config_manager.get_sshout_config()
# 验证服务器配置
self.assertEqual(sshout_config["server"]["hostname"], "sshout.test.com")
self.assertEqual(sshout_config["server"]["port"], 22333)
# 验证路径转换(相对路径应该转换为绝对路径)
expected_absolute_path = os.path.join(self.test_dir, "relative/path/to/key")
self.assertEqual(sshout_config["ssh_key"]["private_key_path"], expected_absolute_path)
def test_absolute_path_not_modified(self):
"""测试绝对路径不被修改"""
absolute_path = "/absolute/path/to/key"
toml_content = f"""
[sshout]
[sshout.ssh_key]
private_key_path = "{absolute_path}"
"""
self._create_test_config("absolute.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("absolute")
sshout_config = config_manager.get_sshout_config()
# 绝对路径应该保持不变
self.assertEqual(sshout_config["ssh_key"]["private_key_path"], absolute_path)
def test_agent_config_method(self):
"""测试get_agent_config方法"""
toml_content = """
[agent]
default_mode = "yolo"
model = "claude-sonnet-4"
api_timeout = 60
[other]
value = "ignored"
"""
self._create_test_config("agent.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("agent")
agent_config = config_manager.get_agent_config()
self.assertEqual(agent_config["default_mode"], "yolo")
self.assertEqual(agent_config["model"], "claude-sonnet-4")
self.assertEqual(agent_config["api_timeout"], 60)
self.assertNotIn("other", agent_config)
def test_cli_config_method(self):
"""测试get_cli_config方法"""
toml_content = """
[cli]
max_command_history = 500
prompt_style = "green"
show_progress = false
"""
self._create_test_config("cli.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("cli")
cli_config = config_manager.get_cli_config()
self.assertEqual(cli_config["max_command_history"], 500)
self.assertEqual(cli_config["prompt_style"], "green")
self.assertFalse(cli_config["show_progress"])
def test_reload_method(self):
"""测试配置重新加载功能"""
# 初始配置
initial_content = """
[test]
value = "initial"
"""
config_path = self._create_test_config("reload.toml", initial_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("reload")
# 验证初始值
self.assertEqual(config_manager.get("test.value"), "initial")
# 修改配置文件
updated_content = """
[test]
value = "updated"
"""
with open(config_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
# 重新加载配置
config_manager.reload()
# 验证更新后的值
self.assertEqual(config_manager.get("test.value"), "updated")
def test_config_manager_init_with_config_not_found(self):
"""测试ConfigManager初始化时配置文件不存在(覆盖第46-47行)"""
with patch.object(ConfigManager, '_get_project_root', return_value='/test/root'), \
patch('os.path.exists', return_value=False):
# 配置文件不存在时应该抛出FileNotFoundError
with self.assertRaises(FileNotFoundError) as context:
ConfigManager('nonexistent')
self.assertIn("配置文件不存在", str(context.exception))
def test_get_mcp_config(self):
"""测试get_mcp_config方法(覆盖第113行)"""
toml_content = """
[mcp]
servers = ["server1", "server2"]
[mcp.tools]
enabled = true
"""
self._create_test_config("mcp.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("mcp")
mcp_config = config_manager.get_mcp_config()
self.assertEqual(mcp_config["servers"], ["server1", "server2"])
self.assertTrue(mcp_config["tools"]["enabled"])
def test_get_logging_config(self):
"""测试get_logging_config方法(覆盖第117行)"""
toml_content = """
[logging]
level = "DEBUG"
colored = true
file_handler = "/var/log/claude.log"
"""
self._create_test_config("logging.toml", toml_content)
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
config_manager = ConfigManager("logging")
logging_config = config_manager.get_logging_config()
self.assertEqual(logging_config["level"], "DEBUG")
self.assertTrue(logging_config["colored"])
self.assertEqual(logging_config["file_handler"], "/var/log/claude.log")
def test_config_file_format_error(self):
"""测试配置文件格式错误的处理(覆盖第53行)"""
# 创建格式错误的TOML文件
with patch('builtins.open', mock_open(read_data='invalid toml')), \
patch('os.path.exists', return_value=True), \
patch.object(ConfigManager, '_get_project_root'):
# 配置文件格式错误时应该抛出ValueError
with self.assertRaises(ValueError) as context:
ConfigManager('invalid')
self.assertIn("配置文件格式错误", str(context.exception))
def test_get_project_root_path_resolution(self):
"""测试_get_project_root的路径解析(覆盖第38-40行)"""
# 测试项目根目录解析逻辑
with patch('os.path.abspath') as mock_abspath, \
patch('os.path.dirname') as mock_dirname:
# 模拟当前文件路径
mock_abspath.return_value = '/project/src/claude_agent/utils/config.py'
# dirname调用4次回到项目根目录
mock_dirname.side_effect = [
'/project/src/claude_agent/utils', # 第1次
'/project/src/claude_agent', # 第2次
'/project/src', # 第3次
'/project' # 第4次 - 项目根目录
]
config_manager = ConfigManager.__new__(ConfigManager) # 避免__init__
result = config_manager._get_project_root()
self.assertEqual(result, '/project')
class TestGlobalConfigFunctions(unittest.TestCase):
"""测试全局配置函数"""
def setUp(self):
"""设置测试环境"""
# 清理全局状态
import src.claude_agent.utils.config
src.claude_agent.utils.config._config_manager = None
self.test_dir = tempfile.mkdtemp()
self.config_dir = os.path.join(self.test_dir, "configs")
os.makedirs(self.config_dir, exist_ok=True)
def tearDown(self):
"""清理测试环境"""
# 清理全局状态
import src.claude_agent.utils.config
src.claude_agent.utils.config._config_manager = None
shutil.rmtree(self.test_dir, ignore_errors=True)
def _create_test_config(self, filename: str, content: str):
"""创建测试配置文件"""
config_path = os.path.join(self.config_dir, filename)
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
return config_path
def test_get_config_manager_singleton(self):
"""测试get_config_manager的单例模式"""
self._create_test_config("default.toml", "[test]\nvalue = 1")
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
# 第一次调用
manager1 = get_config_manager()
# 第二次调用应该返回同一个实例
manager2 = get_config_manager()
self.assertIs(manager1, manager2)
def test_get_config_manager_with_different_config(self):
"""测试使用不同配置名称时创建新实例"""
self._create_test_config("default.toml", "[test]\nvalue = 1")
self._create_test_config("custom.toml", "[test]\nvalue = 2")
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
# 默认配置
manager1 = get_config_manager()
# 指定不同配置应该创建新实例
manager2 = get_config_manager("custom")
self.assertIsNot(manager1, manager2)
self.assertEqual(manager1.config_name, "default")
self.assertEqual(manager2.config_name, "custom")
@patch.dict(os.environ, {'CLAUDE_CONFIG': 'env_config'})
def test_get_config_manager_with_env_var(self):
"""测试使用环境变量指定配置"""
self._create_test_config("env_config.toml", "[test]\nvalue = 3")
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
manager = get_config_manager()
self.assertEqual(manager.config_name, "env_config")
def test_get_config_manager_singleton_behavior_advanced(self):
"""测试get_config_manager的高级单例行为"""
# 保存原始实例
from src.claude_agent.utils.config import _config_manager as original_manager
try:
# 重置全局实例
import src.claude_agent.utils.config as config_module
config_module._config_manager = None
self._create_test_config("test.toml", "[test]\nvalue = 1")
with patch.object(ConfigManager, '_get_project_root', return_value=self.test_dir):
manager1 = get_config_manager('test')
manager2 = get_config_manager() # 不传参数,应该返回相同实例
# 应该返回同一个实例
self.assertIs(manager1, manager2)
finally:
# 恢复原始实例
import src.claude_agent.utils.config as config_module
config_module._config_manager = original_manager
if __name__ == '__main__':
unittest.main(verbosity=2)