| """ |
| 配置管理器单元测试 |
| 测试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) |