"""真实 InferenceEngine 工厂测试(env 驱动,无需真实密钥)。 覆盖: - 缺 API Key → 抛 LLMNotConfiguredError - GENESIS_ 前缀环境变量构造 - 裸 DEEPSEEK_API_KEY / LLM_BASE_URL 兼容 - .env 文件加载 """ from __future__ import annotations import os from pathlib import Path import pytest from genesis.inference.exceptions import LLMNotConfiguredError from genesis.inference.factory import build_inference_engine, _load_dotenv def _isolate(monkeypatch: pytest.MonkeyPatch) -> None: """屏蔽 .env 自动加载与所有相关环境变量,保证测试确定性。""" monkeypatch.setattr("genesis.inference.factory._load_dotenv", lambda *a, **k: None) for k in ( "GENESIS_INFERENCE__API_KEY", "GENESIS_INFERENCE__BASE_URL", "GENESIS_INFERENCE__MODEL", "GENESIS_INFERENCE__FALLBACK_MODEL", "DEEPSEEK_API_KEY", "LLM_BASE_URL", "LLM_MODEL", "LLM_FALLBACK_MODEL", ): monkeypatch.delenv(k, raising=False) def test_missing_key_raises(monkeypatch: pytest.MonkeyPatch) -> None: _isolate(monkeypatch) with pytest.raises(LLMNotConfiguredError): build_inference_engine() def test_builds_from_genesis_env(monkeypatch: pytest.MonkeyPatch) -> None: _isolate(monkeypatch) monkeypatch.setenv("GENESIS_INFERENCE__API_KEY", "sk-test") monkeypatch.setenv("GENESIS_INFERENCE__BASE_URL", "https://llm.example.com") monkeypatch.setenv("GENESIS_INFERENCE__MODEL", "my-model") monkeypatch.setenv("GENESIS_INFERENCE__FALLBACK_MODEL", "my-fallback") eng = build_inference_engine() assert eng._model_names(None) == ["my-model", "my-fallback"] assert eng._client._base_url == "https://llm.example.com" assert eng._client._api_key == "sk-test" def test_builds_from_bare_env(monkeypatch: pytest.MonkeyPatch) -> None: _isolate(monkeypatch) monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-bare") monkeypatch.setenv("LLM_BASE_URL", "https://bare.example.com") eng = build_inference_engine() assert eng._client._api_key == "sk-bare" assert eng._client._base_url == "https://bare.example.com" assert eng._model_names(None) == ["deepseek-chat", "qwen-max"] def test_default_base_url_when_absent(monkeypatch: pytest.MonkeyPatch) -> None: _isolate(monkeypatch) monkeypatch.setenv("GENESIS_INFERENCE__API_KEY", "sk-test") eng = build_inference_engine() assert eng._client._base_url == "https://api.deepseek.com" def test_load_dotenv(tmp_path: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch) -> None: _isolate(monkeypatch) env_file = tmp_path / ".env" env_file.write_text( 'GENESIS_INFERENCE__API_KEY=sk-dot\n' 'GENESIS_INFERENCE__BASE_URL=https://dot.example.com\n', encoding="utf-8", ) _load_dotenv(env_file) assert os.environ.get("GENESIS_INFERENCE__API_KEY") == "sk-dot" assert os.environ.get("GENESIS_INFERENCE__BASE_URL") == "https://dot.example.com" for k in ("GENESIS_INFERENCE__API_KEY", "GENESIS_INFERENCE__BASE_URL"): monkeypatch.delenv(k, raising=False) def test_load_dotenv_missing_file_noop(tmp_path: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch) -> None: # 路径不存在时不应抛异常,且不引入任何变量 for k in ("GENESIS_INFERENCE__API_KEY", "GENESIS_INFERENCE__BASE_URL", "GENESIS_INFERENCE__MODEL"): monkeypatch.delenv(k, raising=False) _load_dotenv(tmp_path / "nonexistent.env") assert os.environ.get("GENESIS_INFERENCE__API_KEY") is None def test_load_dotenv_skips_comments_and_strips_quotes(tmp_path: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch) -> None: for k in ( "GENESIS_INFERENCE__API_KEY", "GENESIS_INFERENCE__BASE_URL", "GENESIS_INFERENCE__MODEL", ): monkeypatch.delenv(k, raising=False) env_file = tmp_path / ".env" env_file.write_text( "# 这是注释行\n" "GENESIS_INFERENCE__API_KEY=\"sk-quoted\"\n" "GENESIS_INFERENCE__BASE_URL='https://quoted.example.com'\n" "MALFORMED_LINE_WITHOUT_EQUALS\n" "GENESIS_INFERENCE__MODEL=deepseek-chat\n", encoding="utf-8", ) _load_dotenv(env_file) assert os.environ["GENESIS_INFERENCE__API_KEY"] == "sk-quoted" assert os.environ["GENESIS_INFERENCE__BASE_URL"] == "https://quoted.example.com" assert os.environ["GENESIS_INFERENCE__MODEL"] == "deepseek-chat" for k in ( "GENESIS_INFERENCE__API_KEY", "GENESIS_INFERENCE__BASE_URL", "GENESIS_INFERENCE__MODEL", ): monkeypatch.delenv(k, raising=False)