Files

1 line
52 KiB
JSON

{"repo": "christostjc/sinan-agent", "files": {"tests/test_config.py": {"d62ab994c9d29bef3696342b4bb3db87af562802": {"t": 1785746875.5433357, "n": 10284, "h": "dc5870ae43048c5e", "c": "\"\"\"\nconfig.py 单元测试。\n\n覆盖:\n- _deep_merge: 字典深度合并\n- load_project_config: YAML 加载\n- load_settings: 三级优先级合并\n- get_model_config / get_tools_config: 配置提取\n\"\"\"\n\nimport json\nimport tempfile\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\n\nfrom agent.config.legacy import (\n DEFAULT_SETTINGS,\n _deep_merge,\n _migrate_old_settings,\n get_model_config,\n get_tools_config,\n load_project_config,\n load_settings,\n)\n\n\n# ---------------------------------------------------------------------------\n# _deep_merge\n# ---------------------------------------------------------------------------\n\n\nclass TestDeepMerge:\n \"\"\"测试 _deep_merge 深度合并。\"\"\"\n\n def test_flat_merge(self):\n base = {\"a\": 1, \"b\": 2}\n override = {\"b\": 3, \"c\": 4}\n result = _deep_merge(base, override)\n assert result == {\"a\": 1, \"b\": 3, \"c\": 4}\n\n def test_nested_merge(self):\n base = {\"model\": {\"provider\": \"ollama\", \"name\": \"qwen\"}}\n override = {\"model\": {\"name\": \"llama3\"}}\n result = _deep_merge(base, override)\n assert result == {\"model\": {\"provider\": \"ollama\", \"name\": \"llama3\"}}\n\n def test_deep_nested_merge(self):\n base = {\"a\": {\"b\": {\"c\": 1, \"d\": 2}}}\n override = {\"a\": {\"b\": {\"d\": 3, \"e\": 4}}}\n result = _deep_merge(base, override)\n assert result == {\"a\": {\"b\": {\"c\": 1, \"d\": 3, \"e\": 4}}}\n\n def test_override_dict_with_scalar(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": \"string\"}\n result = _deep_merge(base, override)\n assert result == {\"a\": \"string\"}\n\n def test_empty_override(self):\n base = {\"a\": 1, \"b\": 2}\n result = _deep_merge(base, {})\n assert result == {\"a\": 1, \"b\": 2}\n\n def test_does_not_mutate_base(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": {\"c\": 2}}\n _deep_merge(base, override)\n assert base == {\"a\": {\"b\": 1}}\n\n\n# ---------------------------------------------------------------------------\n# load_project_config\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadProjectConfig:\n \"\"\"测试 load_project_config YAML 加载。\"\"\"\n\n def test_returns_dict_on_missing_file(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")):\n result = load_project_config()\n assert result == {}\n\n def test_loads_valid_yaml(self):\n yaml_content = \"model:\\n provider: test\\n name: test-model\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result[\"model\"][\"provider\"] == \"test\"\n\n def test_returns_empty_on_invalid_yaml(self):\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(\"- just\\n- a\\n- list\\n\")\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result == {}\n\n\n# ---------------------------------------------------------------------------\n# load_settings\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadSettings:\n \"\"\"测试 load_settings 三级优先级。\"\"\"\n\n def test_returns_defaults_when_no_files(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"] == DEFAULT_SETTINGS[\"availableModels\"]\n\n def test_project_config_overrides_defaults(self):\n yaml_content = \"availableModels:\\n - label: Test\\n model: gpt-4o\\n provider: openai\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"provider\"] == \"openai\"\n assert settings[\"availableModels\"][0][\"model\"] == \"gpt-4o\"\n\n def test_user_config_highest_priority(self):\n user_settings = {\"availableModels\": [{\"label\": \"My\", \"model\": \"my-custom-model\", \"provider\": \"openai\"}]}\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".json\", delete=False) as f:\n json.dump(user_settings, f)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(f.name)):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"model\"] == \"my-custom-model\"\n\n\n# ---------------------------------------------------------------------------\n# get_model_config / get_tools_config\n# ---------------------------------------------------------------------------\n\n\nclass TestConfigExtractors:\n \"\"\"测试配置提取函数。\"\"\"\n\n def test_get_model_config_new_format(self):\n settings = {\n \"availableModels\": [\n {\"label\": \"Claude 3\", \"model\": \"claude-3\", \"provider\": \"claude\"},\n ],\n \"maxTokens\": 8192,\n \"temperature\": 0.7,\n \"thinking\": None,\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"claude\"\n assert result[\"name\"] == \"claude-3\"\n assert result[\"params\"][\"max_tokens\"] == 8192\n assert result[\"params\"][\"temperature\"] == 0.7\n\n def test_get_model_config_old_format_migrated(self):\n \"\"\"旧格式 (model 为嵌套对象) 应自动迁移。\"\"\"\n settings = {\n \"model\": {\"provider\": \"openai\", \"name\": \"gpt-4o\", \"params\": {\"max_tokens\": 2048}}\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"openai\"\n assert result[\"name\"] == \"gpt-4o\"\n assert result[\"params\"][\"max_tokens\"] == 2048\n\n def test_get_model_config_defaults(self):\n result = get_model_config({})\n # 空配置时回退到 DEFAULT_SETTINGS 的 availableModels[0]\n assert result[\"provider\"] == \"ollama\"\n assert result[\"name\"] == \"qwen2.5:7b\"\n\n def test_migrate_old_settings_nested_dict(self):\n old = {\n \"model\": {\n \"provider\": \"claude\",\n \"name\": \"deepseek-v4-pro\",\n \"params\": {\"max_tokens\": 4096, \"temperature\": None, \"thinking\": None},\n \"catalog\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}],\n },\n \"env\": {\"ANTHROPIC_API_KEY\": \"sk-test\"},\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert result[\"availableModels\"] == [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}]\n assert result[\"maxTokens\"] == 4096\n assert result[\"env\"] == {\"ANTHROPIC_API_KEY\": \"sk-test\"}\n\n def test_migrate_old_settings_transition_format(self):\n \"\"\"过渡格式 (model 字符串 + modelProvider) 应迁移。\"\"\"\n old = {\n \"model\": \"deepseek-v4-pro\",\n \"modelProvider\": \"claude\",\n \"availableModels\": [\n {\"label\": \"DeepSeek V4 Pro\", \"model\": \"deepseek-v4-pro\", \"provider\": \"claude\"},\n ],\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert \"modelProvider\" not in result\n assert result[\"availableModels\"][0][\"model\"] == \"deepseek-v4-pro\"\n\n def test_migrate_new_format_passthrough(self):\n \"\"\"新格式不应被修改。\"\"\"\n new = {\"availableModels\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"ollama\"}]}\n result = _migrate_old_settings(new)\n assert result == new\n\n def test_get_tools_config_empty(self):\n result = get_tools_config({})\n assert result == {}\n\n def test_get_tools_config_with_data(self):\n settings = {\"tools\": {\"serial\": {\"max_bytes\": 5120}}}\n result = get_tools_config(settings)\n assert result == {\"serial\": {\"max_bytes\": 5120}}\n\n\n# ---------------------------------------------------------------------------\n# #2: config.defaults.yaml 路径与 tools.* 契约\n# ---------------------------------------------------------------------------\n\n\nclass TestProjectConfigToolsContract:\n \"\"\"#2: 修复后 config.defaults.yaml 应被加载且 tools.* 精确生效。\"\"\"\n\n def test_project_config_file_shipped_inside_package(self):\n # 配置文件必须随包分发:与 legacy.py 同目录 (agent/config/)。\n # 放在 repo 根目录时 pip/wheel 安装不会打包它,项目默认层静默失效。\n from agent.config import legacy\n from agent.config.legacy import PROJECT_CONFIG_FILE\n assert PROJECT_CONFIG_FILE.name == \"config.defaults.yaml\"\n assert PROJECT_CONFIG_FILE.exists()\n assert PROJECT_CONFIG_FILE.parent == Path(legacy.__file__).resolve().parent\n assert PROJECT_CONFIG_FILE.parent.name == \"config\"\n assert PROJECT_CONFIG_FILE.parent.parent.name == \"agent\"\n\n def test_tools_config_exact_values_from_yaml(self):\n # patch SETTINGS_FILE 到不存在,避免本机 ~/.sinan/settings.json 覆盖 project yaml\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n tools = get_tools_config()\n assert tools[\"serial\"][\"max_bytes\"] == 10240\n assert tools[\"serial\"][\"max_duration_sec\"] == 30\n assert tools[\"device_node\"][\"default_port\"] == 5555\n assert tools[\"device_node\"][\"timeout_sec\"] == 5.0\n assert tools[\"max_tool_depth\"] == 25\n\n def test_dead_keys_removed(self):\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n project = load_project_config()\n assert \"platforms\" not in project\n assert \"build_systems\" not in project\n assert \"flash_tools\" not in project\n"}, "c113927efa6fc99026ffac4dd844e705fe1c0295": {"t": 1785768365.7195656, "n": 10284, "h": "dc5870ae43048c5e", "c": "\"\"\"\nconfig.py 单元测试。\n\n覆盖:\n- _deep_merge: 字典深度合并\n- load_project_config: YAML 加载\n- load_settings: 三级优先级合并\n- get_model_config / get_tools_config: 配置提取\n\"\"\"\n\nimport json\nimport tempfile\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\n\nfrom agent.config.legacy import (\n DEFAULT_SETTINGS,\n _deep_merge,\n _migrate_old_settings,\n get_model_config,\n get_tools_config,\n load_project_config,\n load_settings,\n)\n\n\n# ---------------------------------------------------------------------------\n# _deep_merge\n# ---------------------------------------------------------------------------\n\n\nclass TestDeepMerge:\n \"\"\"测试 _deep_merge 深度合并。\"\"\"\n\n def test_flat_merge(self):\n base = {\"a\": 1, \"b\": 2}\n override = {\"b\": 3, \"c\": 4}\n result = _deep_merge(base, override)\n assert result == {\"a\": 1, \"b\": 3, \"c\": 4}\n\n def test_nested_merge(self):\n base = {\"model\": {\"provider\": \"ollama\", \"name\": \"qwen\"}}\n override = {\"model\": {\"name\": \"llama3\"}}\n result = _deep_merge(base, override)\n assert result == {\"model\": {\"provider\": \"ollama\", \"name\": \"llama3\"}}\n\n def test_deep_nested_merge(self):\n base = {\"a\": {\"b\": {\"c\": 1, \"d\": 2}}}\n override = {\"a\": {\"b\": {\"d\": 3, \"e\": 4}}}\n result = _deep_merge(base, override)\n assert result == {\"a\": {\"b\": {\"c\": 1, \"d\": 3, \"e\": 4}}}\n\n def test_override_dict_with_scalar(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": \"string\"}\n result = _deep_merge(base, override)\n assert result == {\"a\": \"string\"}\n\n def test_empty_override(self):\n base = {\"a\": 1, \"b\": 2}\n result = _deep_merge(base, {})\n assert result == {\"a\": 1, \"b\": 2}\n\n def test_does_not_mutate_base(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": {\"c\": 2}}\n _deep_merge(base, override)\n assert base == {\"a\": {\"b\": 1}}\n\n\n# ---------------------------------------------------------------------------\n# load_project_config\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadProjectConfig:\n \"\"\"测试 load_project_config YAML 加载。\"\"\"\n\n def test_returns_dict_on_missing_file(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")):\n result = load_project_config()\n assert result == {}\n\n def test_loads_valid_yaml(self):\n yaml_content = \"model:\\n provider: test\\n name: test-model\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result[\"model\"][\"provider\"] == \"test\"\n\n def test_returns_empty_on_invalid_yaml(self):\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(\"- just\\n- a\\n- list\\n\")\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result == {}\n\n\n# ---------------------------------------------------------------------------\n# load_settings\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadSettings:\n \"\"\"测试 load_settings 三级优先级。\"\"\"\n\n def test_returns_defaults_when_no_files(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"] == DEFAULT_SETTINGS[\"availableModels\"]\n\n def test_project_config_overrides_defaults(self):\n yaml_content = \"availableModels:\\n - label: Test\\n model: gpt-4o\\n provider: openai\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"provider\"] == \"openai\"\n assert settings[\"availableModels\"][0][\"model\"] == \"gpt-4o\"\n\n def test_user_config_highest_priority(self):\n user_settings = {\"availableModels\": [{\"label\": \"My\", \"model\": \"my-custom-model\", \"provider\": \"openai\"}]}\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".json\", delete=False) as f:\n json.dump(user_settings, f)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(f.name)):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"model\"] == \"my-custom-model\"\n\n\n# ---------------------------------------------------------------------------\n# get_model_config / get_tools_config\n# ---------------------------------------------------------------------------\n\n\nclass TestConfigExtractors:\n \"\"\"测试配置提取函数。\"\"\"\n\n def test_get_model_config_new_format(self):\n settings = {\n \"availableModels\": [\n {\"label\": \"Claude 3\", \"model\": \"claude-3\", \"provider\": \"claude\"},\n ],\n \"maxTokens\": 8192,\n \"temperature\": 0.7,\n \"thinking\": None,\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"claude\"\n assert result[\"name\"] == \"claude-3\"\n assert result[\"params\"][\"max_tokens\"] == 8192\n assert result[\"params\"][\"temperature\"] == 0.7\n\n def test_get_model_config_old_format_migrated(self):\n \"\"\"旧格式 (model 为嵌套对象) 应自动迁移。\"\"\"\n settings = {\n \"model\": {\"provider\": \"openai\", \"name\": \"gpt-4o\", \"params\": {\"max_tokens\": 2048}}\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"openai\"\n assert result[\"name\"] == \"gpt-4o\"\n assert result[\"params\"][\"max_tokens\"] == 2048\n\n def test_get_model_config_defaults(self):\n result = get_model_config({})\n # 空配置时回退到 DEFAULT_SETTINGS 的 availableModels[0]\n assert result[\"provider\"] == \"ollama\"\n assert result[\"name\"] == \"qwen2.5:7b\"\n\n def test_migrate_old_settings_nested_dict(self):\n old = {\n \"model\": {\n \"provider\": \"claude\",\n \"name\": \"deepseek-v4-pro\",\n \"params\": {\"max_tokens\": 4096, \"temperature\": None, \"thinking\": None},\n \"catalog\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}],\n },\n \"env\": {\"ANTHROPIC_API_KEY\": \"sk-test\"},\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert result[\"availableModels\"] == [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}]\n assert result[\"maxTokens\"] == 4096\n assert result[\"env\"] == {\"ANTHROPIC_API_KEY\": \"sk-test\"}\n\n def test_migrate_old_settings_transition_format(self):\n \"\"\"过渡格式 (model 字符串 + modelProvider) 应迁移。\"\"\"\n old = {\n \"model\": \"deepseek-v4-pro\",\n \"modelProvider\": \"claude\",\n \"availableModels\": [\n {\"label\": \"DeepSeek V4 Pro\", \"model\": \"deepseek-v4-pro\", \"provider\": \"claude\"},\n ],\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert \"modelProvider\" not in result\n assert result[\"availableModels\"][0][\"model\"] == \"deepseek-v4-pro\"\n\n def test_migrate_new_format_passthrough(self):\n \"\"\"新格式不应被修改。\"\"\"\n new = {\"availableModels\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"ollama\"}]}\n result = _migrate_old_settings(new)\n assert result == new\n\n def test_get_tools_config_empty(self):\n result = get_tools_config({})\n assert result == {}\n\n def test_get_tools_config_with_data(self):\n settings = {\"tools\": {\"serial\": {\"max_bytes\": 5120}}}\n result = get_tools_config(settings)\n assert result == {\"serial\": {\"max_bytes\": 5120}}\n\n\n# ---------------------------------------------------------------------------\n# #2: config.defaults.yaml 路径与 tools.* 契约\n# ---------------------------------------------------------------------------\n\n\nclass TestProjectConfigToolsContract:\n \"\"\"#2: 修复后 config.defaults.yaml 应被加载且 tools.* 精确生效。\"\"\"\n\n def test_project_config_file_shipped_inside_package(self):\n # 配置文件必须随包分发:与 legacy.py 同目录 (agent/config/)。\n # 放在 repo 根目录时 pip/wheel 安装不会打包它,项目默认层静默失效。\n from agent.config import legacy\n from agent.config.legacy import PROJECT_CONFIG_FILE\n assert PROJECT_CONFIG_FILE.name == \"config.defaults.yaml\"\n assert PROJECT_CONFIG_FILE.exists()\n assert PROJECT_CONFIG_FILE.parent == Path(legacy.__file__).resolve().parent\n assert PROJECT_CONFIG_FILE.parent.name == \"config\"\n assert PROJECT_CONFIG_FILE.parent.parent.name == \"agent\"\n\n def test_tools_config_exact_values_from_yaml(self):\n # patch SETTINGS_FILE 到不存在,避免本机 ~/.sinan/settings.json 覆盖 project yaml\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n tools = get_tools_config()\n assert tools[\"serial\"][\"max_bytes\"] == 10240\n assert tools[\"serial\"][\"max_duration_sec\"] == 30\n assert tools[\"device_node\"][\"default_port\"] == 5555\n assert tools[\"device_node\"][\"timeout_sec\"] == 5.0\n assert tools[\"max_tool_depth\"] == 25\n\n def test_dead_keys_removed(self):\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n project = load_project_config()\n assert \"platforms\" not in project\n assert \"build_systems\" not in project\n assert \"flash_tools\" not in project\n"}, "384f980644ad2b999babccfb44a781d1b2d347da": {"t": 1785768366.980195, "n": 9990, "h": "f4c1ee12a3dd9327", "c": "\"\"\"\nconfig.py 单元测试。\n\n覆盖:\n- _deep_merge: 字典深度合并\n- load_project_config: YAML 加载\n- load_settings: 三级优先级合并\n- get_model_config / get_tools_config: 配置提取\n\"\"\"\n\nimport json\nimport tempfile\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\n\nfrom agent.config.legacy import (\n DEFAULT_SETTINGS,\n _deep_merge,\n _migrate_old_settings,\n get_model_config,\n get_tools_config,\n load_project_config,\n load_settings,\n)\n\n\n# ---------------------------------------------------------------------------\n# _deep_merge\n# ---------------------------------------------------------------------------\n\n\nclass TestDeepMerge:\n \"\"\"测试 _deep_merge 深度合并。\"\"\"\n\n def test_flat_merge(self):\n base = {\"a\": 1, \"b\": 2}\n override = {\"b\": 3, \"c\": 4}\n result = _deep_merge(base, override)\n assert result == {\"a\": 1, \"b\": 3, \"c\": 4}\n\n def test_nested_merge(self):\n base = {\"model\": {\"provider\": \"ollama\", \"name\": \"qwen\"}}\n override = {\"model\": {\"name\": \"llama3\"}}\n result = _deep_merge(base, override)\n assert result == {\"model\": {\"provider\": \"ollama\", \"name\": \"llama3\"}}\n\n def test_deep_nested_merge(self):\n base = {\"a\": {\"b\": {\"c\": 1, \"d\": 2}}}\n override = {\"a\": {\"b\": {\"d\": 3, \"e\": 4}}}\n result = _deep_merge(base, override)\n assert result == {\"a\": {\"b\": {\"c\": 1, \"d\": 3, \"e\": 4}}}\n\n def test_override_dict_with_scalar(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": \"string\"}\n result = _deep_merge(base, override)\n assert result == {\"a\": \"string\"}\n\n def test_empty_override(self):\n base = {\"a\": 1, \"b\": 2}\n result = _deep_merge(base, {})\n assert result == {\"a\": 1, \"b\": 2}\n\n def test_does_not_mutate_base(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": {\"c\": 2}}\n _deep_merge(base, override)\n assert base == {\"a\": {\"b\": 1}}\n\n\n# ---------------------------------------------------------------------------\n# load_project_config\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadProjectConfig:\n \"\"\"测试 load_project_config YAML 加载。\"\"\"\n\n def test_returns_dict_on_missing_file(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")):\n result = load_project_config()\n assert result == {}\n\n def test_loads_valid_yaml(self):\n yaml_content = \"model:\\n provider: test\\n name: test-model\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result[\"model\"][\"provider\"] == \"test\"\n\n def test_returns_empty_on_invalid_yaml(self):\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(\"- just\\n- a\\n- list\\n\")\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result == {}\n\n\n# ---------------------------------------------------------------------------\n# load_settings\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadSettings:\n \"\"\"测试 load_settings 三级优先级。\"\"\"\n\n def test_returns_defaults_when_no_files(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"] == DEFAULT_SETTINGS[\"availableModels\"]\n\n def test_project_config_overrides_defaults(self):\n yaml_content = \"availableModels:\\n - label: Test\\n model: gpt-4o\\n provider: openai\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"provider\"] == \"openai\"\n assert settings[\"availableModels\"][0][\"model\"] == \"gpt-4o\"\n\n def test_user_config_highest_priority(self):\n user_settings = {\"availableModels\": [{\"label\": \"My\", \"model\": \"my-custom-model\", \"provider\": \"openai\"}]}\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".json\", delete=False) as f:\n json.dump(user_settings, f)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(f.name)):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"model\"] == \"my-custom-model\"\n\n\n# ---------------------------------------------------------------------------\n# get_model_config / get_tools_config\n# ---------------------------------------------------------------------------\n\n\nclass TestConfigExtractors:\n \"\"\"测试配置提取函数。\"\"\"\n\n def test_get_model_config_new_format(self):\n settings = {\n \"availableModels\": [\n {\"label\": \"Claude 3\", \"model\": \"claude-3\", \"provider\": \"claude\"},\n ],\n \"maxTokens\": 8192,\n \"temperature\": 0.7,\n \"thinking\": None,\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"claude\"\n assert result[\"name\"] == \"claude-3\"\n assert result[\"params\"][\"max_tokens\"] == 8192\n assert result[\"params\"][\"temperature\"] == 0.7\n\n def test_get_model_config_old_format_migrated(self):\n \"\"\"旧格式 (model 为嵌套对象) 应自动迁移。\"\"\"\n settings = {\n \"model\": {\"provider\": \"openai\", \"name\": \"gpt-4o\", \"params\": {\"max_tokens\": 2048}}\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"openai\"\n assert result[\"name\"] == \"gpt-4o\"\n assert result[\"params\"][\"max_tokens\"] == 2048\n\n def test_get_model_config_defaults(self):\n result = get_model_config({})\n # 空配置时回退到 DEFAULT_SETTINGS 的 availableModels[0]\n assert result[\"provider\"] == \"ollama\"\n assert result[\"name\"] == \"qwen2.5:7b\"\n\n def test_migrate_old_settings_nested_dict(self):\n old = {\n \"model\": {\n \"provider\": \"claude\",\n \"name\": \"deepseek-v4-pro\",\n \"params\": {\"max_tokens\": 4096, \"temperature\": None, \"thinking\": None},\n \"catalog\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}],\n },\n \"env\": {\"ANTHROPIC_API_KEY\": \"sk-test\"},\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert result[\"availableModels\"] == [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}]\n assert result[\"maxTokens\"] == 4096\n assert result[\"env\"] == {\"ANTHROPIC_API_KEY\": \"sk-test\"}\n\n def test_migrate_old_settings_transition_format(self):\n \"\"\"过渡格式 (model 字符串 + modelProvider) 应迁移。\"\"\"\n old = {\n \"model\": \"deepseek-v4-pro\",\n \"modelProvider\": \"claude\",\n \"availableModels\": [\n {\"label\": \"DeepSeek V4 Pro\", \"model\": \"deepseek-v4-pro\", \"provider\": \"claude\"},\n ],\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert \"modelProvider\" not in result\n assert result[\"availableModels\"][0][\"model\"] == \"deepseek-v4-pro\"\n\n def test_migrate_new_format_passthrough(self):\n \"\"\"新格式不应被修改。\"\"\"\n new = {\"availableModels\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"ollama\"}]}\n result = _migrate_old_settings(new)\n assert result == new\n\n def test_get_tools_config_empty(self):\n result = get_tools_config({})\n assert result == {}\n\n def test_get_tools_config_with_data(self):\n settings = {\"tools\": {\"serial\": {\"max_bytes\": 5120}}}\n result = get_tools_config(settings)\n assert result == {\"serial\": {\"max_bytes\": 5120}}\n\n\n# ---------------------------------------------------------------------------\n# #2: config.defaults.yaml 路径与 tools.* 契约\n# ---------------------------------------------------------------------------\n\n\nclass TestProjectConfigToolsContract:\n \"\"\"#2: 修复后 config.defaults.yaml 应被加载且 tools.* 精确生效。\"\"\"\n\n def test_project_config_file_points_to_repo_root(self):\n from agent.config.legacy import PROJECT_CONFIG_FILE\n assert PROJECT_CONFIG_FILE.name == \"config.defaults.yaml\"\n assert PROJECT_CONFIG_FILE.exists()\n assert (PROJECT_CONFIG_FILE.parent / \"agent\").is_dir()\n\n def test_tools_config_exact_values_from_yaml(self):\n # patch SETTINGS_FILE 到不存在,避免本机 ~/.sinan/settings.json 覆盖 project yaml\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n tools = get_tools_config()\n assert tools[\"serial\"][\"max_bytes\"] == 10240\n assert tools[\"serial\"][\"max_duration_sec\"] == 30\n assert tools[\"device_node\"][\"default_port\"] == 5555\n assert tools[\"device_node\"][\"timeout_sec\"] == 5.0\n assert tools[\"max_tool_depth\"] == 25\n\n def test_dead_keys_removed(self):\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n project = load_project_config()\n assert \"platforms\" not in project\n assert \"build_systems\" not in project\n assert \"flash_tools\" not in project\n"}, "545c66dd8ac0f204efc3a4f047dc05f71e2a8d98": {"t": 1785768368.1351616, "n": 8566, "h": "4095a993d31b6a95", "c": "\"\"\"\nconfig.py 单元测试。\n\n覆盖:\n- _deep_merge: 字典深度合并\n- load_project_config: YAML 加载\n- load_settings: 三级优先级合并\n- get_model_config / get_tools_config: 配置提取\n\"\"\"\n\nimport json\nimport tempfile\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\n\nfrom agent.config.legacy import (\n DEFAULT_SETTINGS,\n _deep_merge,\n _migrate_old_settings,\n get_model_config,\n get_tools_config,\n load_project_config,\n load_settings,\n)\n\n\n# ---------------------------------------------------------------------------\n# _deep_merge\n# ---------------------------------------------------------------------------\n\n\nclass TestDeepMerge:\n \"\"\"测试 _deep_merge 深度合并。\"\"\"\n\n def test_flat_merge(self):\n base = {\"a\": 1, \"b\": 2}\n override = {\"b\": 3, \"c\": 4}\n result = _deep_merge(base, override)\n assert result == {\"a\": 1, \"b\": 3, \"c\": 4}\n\n def test_nested_merge(self):\n base = {\"model\": {\"provider\": \"ollama\", \"name\": \"qwen\"}}\n override = {\"model\": {\"name\": \"llama3\"}}\n result = _deep_merge(base, override)\n assert result == {\"model\": {\"provider\": \"ollama\", \"name\": \"llama3\"}}\n\n def test_deep_nested_merge(self):\n base = {\"a\": {\"b\": {\"c\": 1, \"d\": 2}}}\n override = {\"a\": {\"b\": {\"d\": 3, \"e\": 4}}}\n result = _deep_merge(base, override)\n assert result == {\"a\": {\"b\": {\"c\": 1, \"d\": 3, \"e\": 4}}}\n\n def test_override_dict_with_scalar(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": \"string\"}\n result = _deep_merge(base, override)\n assert result == {\"a\": \"string\"}\n\n def test_empty_override(self):\n base = {\"a\": 1, \"b\": 2}\n result = _deep_merge(base, {})\n assert result == {\"a\": 1, \"b\": 2}\n\n def test_does_not_mutate_base(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": {\"c\": 2}}\n _deep_merge(base, override)\n assert base == {\"a\": {\"b\": 1}}\n\n\n# ---------------------------------------------------------------------------\n# load_project_config\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadProjectConfig:\n \"\"\"测试 load_project_config YAML 加载。\"\"\"\n\n def test_returns_dict_on_missing_file(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")):\n result = load_project_config()\n assert result == {}\n\n def test_loads_valid_yaml(self):\n yaml_content = \"model:\\n provider: test\\n name: test-model\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result[\"model\"][\"provider\"] == \"test\"\n\n def test_returns_empty_on_invalid_yaml(self):\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(\"- just\\n- a\\n- list\\n\")\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result == {}\n\n\n# ---------------------------------------------------------------------------\n# load_settings\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadSettings:\n \"\"\"测试 load_settings 三级优先级。\"\"\"\n\n def test_returns_defaults_when_no_files(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"] == DEFAULT_SETTINGS[\"availableModels\"]\n\n def test_project_config_overrides_defaults(self):\n yaml_content = \"availableModels:\\n - label: Test\\n model: gpt-4o\\n provider: openai\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)), \\\n mock.patch(\"agent.config.legacy.HAS_YAML\", True), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"provider\"] == \"openai\"\n assert settings[\"availableModels\"][0][\"model\"] == \"gpt-4o\"\n\n def test_user_config_highest_priority(self):\n user_settings = {\"availableModels\": [{\"label\": \"My\", \"model\": \"my-custom-model\", \"provider\": \"openai\"}]}\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".json\", delete=False) as f:\n json.dump(user_settings, f)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")), \\\n mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(f.name)):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"model\"] == \"my-custom-model\"\n\n\n# ---------------------------------------------------------------------------\n# get_model_config / get_tools_config\n# ---------------------------------------------------------------------------\n\n\nclass TestConfigExtractors:\n \"\"\"测试配置提取函数。\"\"\"\n\n def test_get_model_config_new_format(self):\n settings = {\n \"availableModels\": [\n {\"label\": \"Claude 3\", \"model\": \"claude-3\", \"provider\": \"claude\"},\n ],\n \"maxTokens\": 8192,\n \"temperature\": 0.7,\n \"thinking\": None,\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"claude\"\n assert result[\"name\"] == \"claude-3\"\n assert result[\"params\"][\"max_tokens\"] == 8192\n assert result[\"params\"][\"temperature\"] == 0.7\n\n def test_get_model_config_old_format_migrated(self):\n \"\"\"旧格式 (model 为嵌套对象) 应自动迁移。\"\"\"\n settings = {\n \"model\": {\"provider\": \"openai\", \"name\": \"gpt-4o\", \"params\": {\"max_tokens\": 2048}}\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"openai\"\n assert result[\"name\"] == \"gpt-4o\"\n assert result[\"params\"][\"max_tokens\"] == 2048\n\n def test_get_model_config_defaults(self):\n result = get_model_config({})\n # 空配置时回退到 DEFAULT_SETTINGS 的 availableModels[0]\n assert result[\"provider\"] == \"ollama\"\n assert result[\"name\"] == \"qwen2.5:7b\"\n\n def test_migrate_old_settings_nested_dict(self):\n old = {\n \"model\": {\n \"provider\": \"claude\",\n \"name\": \"deepseek-v4-pro\",\n \"params\": {\"max_tokens\": 4096, \"temperature\": None, \"thinking\": None},\n \"catalog\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}],\n },\n \"env\": {\"ANTHROPIC_API_KEY\": \"sk-test\"},\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert result[\"availableModels\"] == [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}]\n assert result[\"maxTokens\"] == 4096\n assert result[\"env\"] == {\"ANTHROPIC_API_KEY\": \"sk-test\"}\n\n def test_migrate_old_settings_transition_format(self):\n \"\"\"过渡格式 (model 字符串 + modelProvider) 应迁移。\"\"\"\n old = {\n \"model\": \"deepseek-v4-pro\",\n \"modelProvider\": \"claude\",\n \"availableModels\": [\n {\"label\": \"DeepSeek V4 Pro\", \"model\": \"deepseek-v4-pro\", \"provider\": \"claude\"},\n ],\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert \"modelProvider\" not in result\n assert result[\"availableModels\"][0][\"model\"] == \"deepseek-v4-pro\"\n\n def test_migrate_new_format_passthrough(self):\n \"\"\"新格式不应被修改。\"\"\"\n new = {\"availableModels\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"ollama\"}]}\n result = _migrate_old_settings(new)\n assert result == new\n\n def test_get_tools_config_empty(self):\n result = get_tools_config({})\n assert result == {}\n\n def test_get_tools_config_with_data(self):\n settings = {\"tools\": {\"serial\": {\"max_bytes\": 5120}}}\n result = get_tools_config(settings)\n assert result == {\"serial\": {\"max_bytes\": 5120}}\n"}, "7460a79fb085d0b58c1926c096badfff97c3e13c": {"t": 1785768369.3476803, "n": 8620, "h": "d01fb4d526622708", "c": "\"\"\"\nconfig.py 单元测试。\n\n覆盖:\n- _deep_merge: 字典深度合并\n- load_project_config: YAML 加载\n- load_settings: 三级优先级合并\n- get_model_config / get_tools_config: 配置提取\n\"\"\"\n\nimport json\nimport tempfile\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\n\nfrom agent.config.legacy import (\n DEFAULT_SETTINGS,\n _deep_merge,\n _migrate_old_settings,\n get_model_config,\n get_tools_config,\n load_project_config,\n load_settings,\n)\n\n\n# ---------------------------------------------------------------------------\n# _deep_merge\n# ---------------------------------------------------------------------------\n\n\nclass TestDeepMerge:\n \"\"\"测试 _deep_merge 深度合并。\"\"\"\n\n def test_flat_merge(self):\n base = {\"a\": 1, \"b\": 2}\n override = {\"b\": 3, \"c\": 4}\n result = _deep_merge(base, override)\n assert result == {\"a\": 1, \"b\": 3, \"c\": 4}\n\n def test_nested_merge(self):\n base = {\"model\": {\"provider\": \"ollama\", \"name\": \"qwen\"}}\n override = {\"model\": {\"name\": \"llama3\"}}\n result = _deep_merge(base, override)\n assert result == {\"model\": {\"provider\": \"ollama\", \"name\": \"llama3\"}}\n\n def test_deep_nested_merge(self):\n base = {\"a\": {\"b\": {\"c\": 1, \"d\": 2}}}\n override = {\"a\": {\"b\": {\"d\": 3, \"e\": 4}}}\n result = _deep_merge(base, override)\n assert result == {\"a\": {\"b\": {\"c\": 1, \"d\": 3, \"e\": 4}}}\n\n def test_override_dict_with_scalar(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": \"string\"}\n result = _deep_merge(base, override)\n assert result == {\"a\": \"string\"}\n\n def test_empty_override(self):\n base = {\"a\": 1, \"b\": 2}\n result = _deep_merge(base, {})\n assert result == {\"a\": 1, \"b\": 2}\n\n def test_does_not_mutate_base(self):\n base = {\"a\": {\"b\": 1}}\n override = {\"a\": {\"c\": 2}}\n _deep_merge(base, override)\n assert base == {\"a\": {\"b\": 1}}\n\n\n# ---------------------------------------------------------------------------\n# load_project_config\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadProjectConfig:\n \"\"\"测试 load_project_config YAML 加载。\"\"\"\n\n def test_returns_dict_on_missing_file(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")):\n result = load_project_config()\n assert result == {}\n\n def test_loads_valid_yaml(self):\n yaml_content = \"model:\\n provider: test\\n name: test-model\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)):\n with mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result[\"model\"][\"provider\"] == \"test\"\n\n def test_returns_empty_on_invalid_yaml(self):\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(\"- just\\n- a\\n- list\\n\")\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)):\n with mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n result = load_project_config()\n assert result == {}\n\n\n# ---------------------------------------------------------------------------\n# load_settings\n# ---------------------------------------------------------------------------\n\n\nclass TestLoadSettings:\n \"\"\"测试 load_settings 三级优先级。\"\"\"\n\n def test_returns_defaults_when_no_files(self):\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")):\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"] == DEFAULT_SETTINGS[\"availableModels\"]\n\n def test_project_config_overrides_defaults(self):\n yaml_content = \"availableModels:\\n - label: Test\\n model: gpt-4o\\n provider: openai\\n\"\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n f.write(yaml_content)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(f.name)):\n with mock.patch(\"agent.config.legacy.HAS_YAML\", True):\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(\"/nonexistent\")):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"provider\"] == \"openai\"\n assert settings[\"availableModels\"][0][\"model\"] == \"gpt-4o\"\n\n def test_user_config_highest_priority(self):\n user_settings = {\"availableModels\": [{\"label\": \"My\", \"model\": \"my-custom-model\", \"provider\": \"openai\"}]}\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".json\", delete=False) as f:\n json.dump(user_settings, f)\n f.flush()\n with mock.patch(\"agent.config.legacy.PROJECT_CONFIG_FILE\", Path(\"/nonexistent\")):\n with mock.patch(\"agent.config.legacy.SETTINGS_FILE\", Path(f.name)):\n settings = load_settings()\n assert settings[\"availableModels\"][0][\"model\"] == \"my-custom-model\"\n\n\n# ---------------------------------------------------------------------------\n# get_model_config / get_tools_config\n# ---------------------------------------------------------------------------\n\n\nclass TestConfigExtractors:\n \"\"\"测试配置提取函数。\"\"\"\n\n def test_get_model_config_new_format(self):\n settings = {\n \"availableModels\": [\n {\"label\": \"Claude 3\", \"model\": \"claude-3\", \"provider\": \"claude\"},\n ],\n \"maxTokens\": 8192,\n \"temperature\": 0.7,\n \"thinking\": None,\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"claude\"\n assert result[\"name\"] == \"claude-3\"\n assert result[\"params\"][\"max_tokens\"] == 8192\n assert result[\"params\"][\"temperature\"] == 0.7\n\n def test_get_model_config_old_format_migrated(self):\n \"\"\"旧格式 (model 为嵌套对象) 应自动迁移。\"\"\"\n settings = {\n \"model\": {\"provider\": \"openai\", \"name\": \"gpt-4o\", \"params\": {\"max_tokens\": 2048}}\n }\n result = get_model_config(settings)\n assert result[\"provider\"] == \"openai\"\n assert result[\"name\"] == \"gpt-4o\"\n assert result[\"params\"][\"max_tokens\"] == 2048\n\n def test_get_model_config_defaults(self):\n result = get_model_config({})\n # 空配置时回退到 DEFAULT_SETTINGS 的 availableModels[0]\n assert result[\"provider\"] == \"ollama\"\n assert result[\"name\"] == \"qwen2.5:7b\"\n\n def test_migrate_old_settings_nested_dict(self):\n old = {\n \"model\": {\n \"provider\": \"claude\",\n \"name\": \"deepseek-v4-pro\",\n \"params\": {\"max_tokens\": 4096, \"temperature\": None, \"thinking\": None},\n \"catalog\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}],\n },\n \"env\": {\"ANTHROPIC_API_KEY\": \"sk-test\"},\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert result[\"availableModels\"] == [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"claude\"}]\n assert result[\"maxTokens\"] == 4096\n assert result[\"env\"] == {\"ANTHROPIC_API_KEY\": \"sk-test\"}\n\n def test_migrate_old_settings_transition_format(self):\n \"\"\"过渡格式 (model 字符串 + modelProvider) 应迁移。\"\"\"\n old = {\n \"model\": \"deepseek-v4-pro\",\n \"modelProvider\": \"claude\",\n \"availableModels\": [\n {\"label\": \"DeepSeek V4 Pro\", \"model\": \"deepseek-v4-pro\", \"provider\": \"claude\"},\n ],\n }\n result = _migrate_old_settings(old)\n assert \"model\" not in result\n assert \"modelProvider\" not in result\n assert result[\"availableModels\"][0][\"model\"] == \"deepseek-v4-pro\"\n\n def test_migrate_new_format_passthrough(self):\n \"\"\"新格式不应被修改。\"\"\"\n new = {\"availableModels\": [{\"label\": \"Test\", \"model\": \"test\", \"provider\": \"ollama\"}]}\n result = _migrate_old_settings(new)\n assert result == new\n\n def test_get_tools_config_empty(self):\n result = get_tools_config({})\n assert result == {}\n\n def test_get_tools_config_with_data(self):\n settings = {\"tools\": {\"serial\": {\"max_bytes\": 5120}}}\n result = get_tools_config(settings)\n assert result == {\"serial\": {\"max_bytes\": 5120}}\n"}}}}