"""多 Provider failover 单元测试(mock 数据,不依赖网络)。""" from types import SimpleNamespace from unittest.mock import patch from app.failover import (FAILOVER_KINDS, failover_candidates, failover_target, should_failover) def _provider(pid, name, models, default_model="", enabled=True): return SimpleNamespace(id=pid, name=name, models=list(models), default_model=default_model or (models[0] if models else ""), enabled=enabled) def _session(providers): return SimpleNamespace(exec=lambda *a, **k: SimpleNamespace(all=lambda: providers)) def test_should_failover_kinds(): assert should_failover("auth") assert should_failover("rate_limit") assert should_failover("connection") assert should_failover("model_not_found") assert not should_failover("other") assert not should_failover("rejected") def test_candidates_excludes_current_and_disabled(): providers = [ _provider(1, "主", ["m1"]), _provider(2, "备一", ["m1"]), _provider(3, "备二", ["m2"], enabled=False), ] session = _session(providers) ids = [p.id for p in failover_candidates(session, 1)] assert ids == [2] def test_target_prefers_same_model(): providers = [ _provider(1, "主", ["deepseek-chat"]), _provider(2, "备一", ["deepseek-chat", "mimo"], default_model="mimo"), _provider(3, "备二", ["glm-4"], default_model="glm-4"), ] session = _session(providers) target = failover_target(session, 1, "deepseek-chat") assert target is not None assert target[0].id == 2 and target[1] == "deepseek-chat" def test_target_falls_back_to_default_model(): providers = [ _provider(1, "主", ["deepseek-chat"]), _provider(2, "备一", ["glm-4"], default_model="glm-4"), ] session = _session(providers) target = failover_target(session, 1, "deepseek-chat") assert target is not None assert target[0].id == 2 and target[1] == "glm-4" def test_target_none_when_no_alternatives(): providers = [_provider(1, "主", ["deepseek-chat"])] session = _session(providers) assert failover_target(session, 1, "deepseek-chat") is None