Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
13 KiB
JSON
1 line
13 KiB
JSON
{"repo": "acefelix/jarvis", "files": {"tests/llm/test_config_loading.py": {"1f70cfbf6f3b157d66198268e97d2aa27e738cad": {"t": 1786068624.0302896, "n": 11472, "h": "ad0aa5290e09dc29", "c": "\"\"\"配置加载/迁移单元测试 — T-03 改进项。\n\n测试覆盖:\n- _apply_toml: TOML dict → Settings 字段映射(顶层、子表、权限模式)\n- apply_env_overrides: 环境变量覆盖(JARVIS_* / MY_AGENT_*)\n- save_last_model / save_custom_model: 模型 TOML 持久化\n- _read_toml: UTF-8 BOM 兼容\n\n@author aceFelix\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport tempfile\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom agent.config.settings import Settings, _apply_toml, _read_toml, load_settings\nfrom agent.config.env import apply_env_overrides\nfrom agent.config.model_registry import save_last_model, save_custom_model\nfrom agent.permissions.modes import PermissionMode\n\n\n# ── _read_toml ──\n\nclass TestReadToml:\n \"\"\"TOML 文件读取测试。\"\"\"\n\n def test_read_nonexistent_file(self) -> None:\n assert _read_toml(Path(\"/nonexistent/settings.toml\")) == {}\n\n def test_read_bom_encoded_file(self) -> None:\n \"\"\"UTF-8 BOM 文件应正确解析。\"\"\"\n with tempfile.NamedTemporaryFile(\n mode=\"wb\", suffix=\".toml\", delete=False\n ) as f:\n f.write(b\"\\xef\\xbb\\xbfmodel = 'gpt-4o'\\nprovider = 'openai'\\n\")\n path = f.name\n try:\n data = _read_toml(Path(path))\n assert data == {\"model\": \"gpt-4o\", \"provider\": \"openai\"}\n finally:\n os.unlink(path)\n\n def test_read_valid_toml(self) -> None:\n with tempfile.NamedTemporaryFile(\n mode=\"w\", suffix=\".toml\", delete=False, encoding=\"utf-8\"\n ) as f:\n f.write(\"provider = 'dashscope'\\nmax_tokens = 4096\\n\")\n path = f.name\n try:\n data = _read_toml(Path(path))\n assert data[\"provider\"] == \"dashscope\"\n assert data[\"max_tokens\"] == 4096\n finally:\n os.unlink(path)\n\n def test_read_empty_file(self) -> None:\n with tempfile.NamedTemporaryFile(\n mode=\"w\", suffix=\".toml\", delete=False, encoding=\"utf-8\"\n ) as f:\n f.write(\"\")\n path = f.name\n try:\n assert _read_toml(Path(path)) == {}\n finally:\n os.unlink(path)\n\n\n# ── _apply_toml ──\n\nclass TestApplyToml:\n \"\"\"TOML → Settings 字段映射测试。\"\"\"\n\n def test_top_level_fields(self) -> None:\n \"\"\"顶层字段应直接映射。\"\"\"\n s = Settings()\n result = _apply_toml(s, {\n \"provider\": \"dashscope\",\n \"model\": \"qwen-plus\",\n \"max_tokens\": 8192,\n \"temperature\": 0.7,\n })\n assert result.provider == \"dashscope\"\n assert result.model == \"qwen-plus\"\n assert result.max_tokens == 8192\n assert result.temperature == 0.7\n\n def test_permission_mode_parsing(self) -> None:\n \"\"\"permission_mode 字符串应解析为枚举。\"\"\"\n s = Settings()\n result = _apply_toml(s, {\"permission_mode\": \"yolo\"})\n assert result.permission_mode == PermissionMode.YOLO\n\n def test_tts_subtable_mapping(self) -> None:\n \"\"\"[tts] 表字段应映射到 tts_* 顶层。\"\"\"\n s = Settings()\n result = _apply_toml(s, {\n \"tts\": {\n \"model\": \"cosyvoice-v3-flash\",\n \"voice\": \"longanlang_v3\",\n \"volume\": 80,\n }\n })\n assert result.tts_model == \"cosyvoice-v3-flash\"\n assert result.tts_voice == \"longanlang_v3\"\n assert result.tts_volume == 80\n\n def test_stt_subtable_mapping(self) -> None:\n s = Settings()\n result = _apply_toml(s, {\n \"stt\": {\n \"model\": \"paraformer-realtime-v2\",\n \"silence_seconds\": 2.0,\n }\n })\n assert result.stt_model == \"paraformer-realtime-v2\"\n assert result.stt_silence_seconds == 2.0\n\n def test_email_subtable_mapping(self) -> None:\n s = Settings()\n result = _apply_toml(s, {\n \"email\": {\n \"enabled\": True,\n \"smtp_host\": \"smtp.163.com\",\n \"smtp_port\": 465,\n }\n })\n assert result.email_enabled is True\n assert result.email_smtp_host == \"smtp.163.com\"\n assert result.email_smtp_port == 465\n\n def test_llm_models_subtable(self) -> None:\n \"\"\"[llm.models] 应映射到 models 字典。\"\"\"\n s = Settings()\n result = _apply_toml(s, {\n \"llm\": {\n \"models\": {\n \"qwen-plus\": \"通义千问 Plus\",\n \"deepseek-chat\": \"DeepSeek Chat\",\n }\n }\n })\n assert result.models == {\n \"qwen-plus\": \"通义千问 Plus\",\n \"deepseek-chat\": \"DeepSeek Chat\",\n }\n\n def test_llm_custom_models_normalization(self) -> None:\n \"\"\"[llm.custom_models] 应规范化 provider_type → api_format 并补充 provider。\"\"\"\n s = Settings()\n result = _apply_toml(s, {\n \"llm\": {\n \"custom_models\": {\n \"my-model\": {\n \"base_url\": \"https://api.example.com/v1\",\n \"api_key\": \"sk-xxx\",\n \"provider_type\": \"openai\", # 旧字段名\n }\n }\n }\n })\n cm = result.custom_models\n assert \"my-model\" in cm\n assert cm[\"my-model\"][\"api_format\"] == \"openai\" # 从 provider_type 规范化\n assert cm[\"my-model\"][\"provider\"] == \"openai\" # 自动补充\n\n def test_empty_data_returns_same_instance(self) -> None:\n \"\"\"空数据应返回原 Settings(无副作用)。\"\"\"\n s = Settings()\n result = _apply_toml(s, {})\n assert result is s\n\n def test_boolean_fields(self) -> None:\n s = Settings(debug=False, verbose=False)\n result = _apply_toml(s, {\"debug\": True, \"verbose\": True})\n assert result.debug is True\n assert result.verbose is True\n\n def test_tools_subtable(self) -> None:\n \"\"\"[tools] 表应映射到 tools_* 字段。\"\"\"\n s = Settings()\n result = _apply_toml(s, {\n \"tools\": {\n \"deferred_loading\": False,\n \"chat_detection\": False,\n }\n })\n assert result.tools_deferred_loading is False\n assert result.tools_chat_detection is False\n\n\n# ── apply_env_overrides ──\n\nclass TestEnvOverrides:\n \"\"\"环境变量覆盖测试。\"\"\"\n\n def test_provider_env_override(self) -> None:\n with patch.dict(os.environ, {\"JARVIS_PROVIDER\": \"deepseek\"}, clear=True):\n s = Settings(provider=\"dashscope\")\n result = apply_env_overrides(s)\n assert result.provider == \"deepseek\"\n\n def test_model_env_override(self) -> None:\n with patch.dict(os.environ, {\"JARVIS_MODEL\": \"gpt-4o\"}, clear=True):\n s = Settings(model=\"qwen\")\n result = apply_env_overrides(s)\n assert result.model == \"gpt-4o\"\n\n def test_permission_mode_env(self) -> None:\n with patch.dict(os.environ, {\"JARVIS_PERMISSION_MODE\": \"yolo\"}, clear=True):\n s = Settings(permission_mode=PermissionMode.DEFAULT)\n result = apply_env_overrides(s)\n assert result.permission_mode == PermissionMode.YOLO\n\n def test_debug_env(self) -> None:\n with patch.dict(os.environ, {\"JARVIS_DEBUG\": \"1\"}, clear=True):\n s = Settings(debug=False)\n result = apply_env_overrides(s)\n assert result.debug is True\n\n def test_api_key_env(self) -> None:\n with patch.dict(os.environ, {\"OPENAI_API_KEY\": \"sk-test123\"}, clear=True):\n s = Settings(api_key=\"\")\n result = apply_env_overrides(s)\n assert result.api_key == \"sk-test123\"\n\n def test_dashscope_api_key_env(self) -> None:\n with patch.dict(os.environ, {\"DASHSCOPE_API_KEY\": \"sk-dash\"}, clear=True):\n s = Settings(dashscope_api_key=\"\", api_key=\"sk-main\")\n result = apply_env_overrides(s)\n assert result.dashscope_api_key == \"sk-dash\"\n # api_key 已有值,不应被覆盖\n assert result.api_key == \"sk-main\"\n\n def test_base_url_env(self) -> None:\n with patch.dict(os.environ, {\"JARVIS_BASE_URL\": \"https://api.example.com/v1\"}, clear=True):\n s = Settings(base_url=\"\")\n result = apply_env_overrides(s)\n assert result.base_url == \"https://api.example.com/v1\"\n\n def test_no_env_vars_no_change(self) -> None:\n with patch.dict(os.environ, {}, clear=True):\n s = Settings(provider=\"dashscope\", model=\"qwen\")\n result = apply_env_overrides(s)\n assert result.provider == \"dashscope\"\n assert result.model == \"qwen\"\n\n def test_my_agent_compat(self) -> None:\n \"\"\"MY_AGENT_* 环境变量兼容性。\"\"\"\n with patch.dict(os.environ, {\"MY_AGENT_PROVIDER\": \"zhipu\"}, clear=True):\n s = Settings(provider=\"dashscope\")\n result = apply_env_overrides(s)\n assert result.provider == \"zhipu\"\n\n\n# ── Model Persistence ──\n\nclass TestModelPersistence:\n \"\"\"模型 TOML 持久化测试。\"\"\"\n\n def test_save_last_model_new_file(self) -> None:\n \"\"\"新文件写入 last_model。\"\"\"\n with tempfile.TemporaryDirectory() as tmpdir:\n home = Path(tmpdir)\n with patch.object(Path, \"home\", return_value=home):\n jarvis_dir = home / \".jarvis\"\n jarvis_dir.mkdir(parents=True, exist_ok=True)\n toml_path = jarvis_dir / \"settings.toml\"\n\n # 首次保存到不存在的文件\n result = save_last_model(\"qwen-plus\")\n assert result is True\n assert toml_path.exists()\n\n content = toml_path.read_text(encoding=\"utf-8\")\n assert 'last_model = \"qwen-plus\"' in content\n\n def test_save_last_model_update_existing(self) -> None:\n \"\"\"已有文件应更新 last_model 而非重复插入。\"\"\"\n with tempfile.TemporaryDirectory() as tmpdir:\n home = Path(tmpdir)\n with patch.object(Path, \"home\", return_value=home):\n jarvis_dir = home / \".jarvis\"\n jarvis_dir.mkdir(parents=True, exist_ok=True)\n toml_path = jarvis_dir / \"settings.toml\"\n toml_path.write_text(\n 'model = \"qwen-plus\"\\nlast_model = \"gpt-4o\"\\n\\n[tts]\\nmodel = \"cosyvoice\"\\n',\n encoding=\"utf-8\",\n )\n\n result = save_last_model(\"deepseek-chat\")\n assert result is True\n\n content = toml_path.read_text(encoding=\"utf-8\")\n assert 'last_model = \"deepseek-chat\"' in content\n # 不应有两个 last_model\n assert content.count(\"last_model\") == 1\n\n def test_save_custom_model_new(self) -> None:\n \"\"\"新增自定义模型到 [llm.custom_models]。\"\"\"\n with tempfile.TemporaryDirectory() as tmpdir:\n home = Path(tmpdir)\n with patch.object(Path, \"home\", return_value=home):\n jarvis_dir = home / \".jarvis\"\n jarvis_dir.mkdir(parents=True, exist_ok=True)\n toml_path = jarvis_dir / \"settings.toml\"\n toml_path.write_text(\n 'model = \"qwen-plus\"\\n\\n[tts]\\nmodel = \"cosyvoice\"\\n',\n encoding=\"utf-8\",\n )\n\n result = save_custom_model(\"my-gpt\", {\n \"base_url\": \"https://api.example.com/v1\",\n \"api_key\": \"sk-xxx\",\n \"provider_type\": \"openai\",\n })\n # save_custom_model 要求文件已存在\n assert result is True\n\n content = toml_path.read_text(encoding=\"utf-8\")\n assert 'my-gpt' in content\n assert 'api_key = \"sk-xxx\"' in content\n"}}}} |