"""模型配置优先级测试:用户 > 管理员 > 环境变量(需求 5 / 15.5)。 验证 ``services.model_config.resolve_chain`` 的优先级、Fallback 拼接与去重, 以及 custom 提供商的可用性判定。导入路径由 tests/conftest.py 设置。 """ from services.model_config import ( # noqa: E402 UserModelConfig, resolve_chain, build_llm_service, ) def _admin(enabled=True, provider="kimi", api_key="admin-key", model="", base_url=""): return {"enabled": enabled, "provider": provider, "api_key": api_key, "model": model, "base_url": base_url} def test_user_config_takes_priority(): """用户自配可用时,链首为用户配置,source=user。""" user = UserModelConfig(provider="deepseek", api_key="user-key", model="deepseek-v4-pro") chain = resolve_chain(user_cfg=user, admin_cfg=_admin(), env={"OPENAI_API_KEY": "envk"}) assert chain.source == "user" assert chain.active_provider == "deepseek" assert chain.active_model == "deepseek-v4-pro" # 管理员与环境档作为 Fallback 仍在链中。 providers = [s.provider for s in chain.specs] assert "kimi" in providers and "openai" in providers def test_admin_used_when_no_user_config(): """用户未配置时,使用管理员配置,source=admin。""" chain = resolve_chain(user_cfg=None, admin_cfg=_admin(provider="zhipu", api_key="ak"), env={"OPENAI_API_KEY": "envk"}) assert chain.source == "admin" assert chain.active_provider == "zhipu" # 环境档兜底仍在。 assert "openai" in [s.provider for s in chain.specs] def test_env_used_when_no_user_or_admin(): """用户与管理员都没有时,回退环境变量,source=env。""" chain = resolve_chain(user_cfg=None, admin_cfg=_admin(enabled=False), env={"DEEPSEEK_API_KEY": "envk"}) assert chain.source == "env" assert chain.active_provider == "deepseek" def test_none_when_nothing_configured(): chain = resolve_chain(user_cfg=None, admin_cfg=None, env={}) assert chain.source == "none" assert chain.specs == [] def test_disabled_admin_is_ignored(): chain = resolve_chain(user_cfg=None, admin_cfg=_admin(enabled=False), env={}) assert chain.source == "none" def test_custom_user_requires_base_url_and_model(): """custom 用户配置缺 base_url/model 时不可用,回退下一档。""" bad = UserModelConfig(provider="custom", api_key="k") # 缺 base_url + model chain = resolve_chain(user_cfg=bad, admin_cfg=_admin(), env={}) assert chain.source == "admin" # 用户档不可用 → 用管理员 good = UserModelConfig(provider="custom", api_key="k", base_url="https://x/v1/chat/completions", model="m") chain2 = resolve_chain(user_cfg=good, admin_cfg=_admin(), env={}) assert chain2.source == "user" assert chain2.active_provider == "custom" assert chain2.active_model == "m" def test_admin_custom_requires_base_url(): """管理员 custom 配置缺 base_url/model 时视为不可用。""" chain = resolve_chain( user_cfg=None, admin_cfg=_admin(provider="custom", api_key="k"), # 缺 base_url+model env={}, ) assert chain.source == "none" def test_dedup_keeps_higher_priority(): """用户与环境同为 deepseek 时去重,仅保留用户档(更高优先级)。""" user = UserModelConfig(provider="deepseek", api_key="user-key") chain = resolve_chain(user_cfg=user, admin_cfg=None, env={"DEEPSEEK_API_KEY": "envk"}) deepseek_specs = [s for s in chain.specs if s.provider == "deepseek"] assert len(deepseek_specs) == 1 assert deepseek_specs[0].api_key == "user-key" def test_build_llm_service_preserves_order(): user = UserModelConfig(provider="deepseek", api_key="uk", model="deepseek-v4-flash") chain = resolve_chain(user_cfg=user, admin_cfg=_admin(provider="kimi", api_key="ak"), env={}) svc = build_llm_service(chain) assert svc._specs[0].provider == "deepseek" assert svc._specs[1].provider == "kimi"