mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 15:01:34 +08:00
* fix(tests): make AIAgent constructor calls self-contained (no env leakage)
Tests in tests/run_agent/ were constructing AIAgent() without passing
both api_key and base_url, then relying on leaked state from other
tests in the same xdist worker (or process-level env vars) to keep
provider resolution happy. Under hermetic conftest + pytest-split,
that state is gone and the tests fail with 'No LLM provider configured'.
Fix: pass both api_key and base_url explicitly on 47 AIAgent()
construction sites across 13 files. AIAgent.__init__ with both set
takes the direct-construction path (line 960 in run_agent.py) and
skips the resolver entirely.
One call site (test_none_base_url_passed_as_none) left alone — that
test asserts behavior for base_url=None specifically.
This is a prerequisite for any future matrix-split or stricter
isolation work, and lands cleanly on its own.
Validation:
- tests/run_agent/ full: 760 passed, 0 failed (local)
- Previously relied on cross-test pollution; now self-contained
* fix(tests): update opencode-go model order assertion to match kimi-k2.5-first
commit 78a74bb promoted kimi-k2.5 to first position in model suggestion
lists but didn't update this test, which has been failing on main since.
Reorder expected list to match the new canonical order.
158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
"""Tests for ordered provider fallback chain (salvage of PR #1761).
|
|
|
|
Extends the single-fallback tests in test_fallback_model.py to cover
|
|
the new list-based ``fallback_providers`` config format and chain
|
|
advancement through multiple providers.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from run_agent import AIAgent
|
|
|
|
|
|
def _make_agent(fallback_model=None):
|
|
"""Create a minimal AIAgent with optional fallback config."""
|
|
with (
|
|
patch("run_agent.get_tool_definitions", return_value=[]),
|
|
patch("run_agent.check_toolset_requirements", return_value={}),
|
|
patch("run_agent.OpenAI"),
|
|
):
|
|
agent = AIAgent(
|
|
api_key="test-key",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
quiet_mode=True,
|
|
skip_context_files=True,
|
|
skip_memory=True,
|
|
fallback_model=fallback_model,
|
|
)
|
|
agent.client = MagicMock()
|
|
return agent
|
|
|
|
|
|
def _mock_client(base_url="https://openrouter.ai/api/v1", api_key="fb-key"):
|
|
mock = MagicMock()
|
|
mock.base_url = base_url
|
|
mock.api_key = api_key
|
|
return mock
|
|
|
|
|
|
# ── Chain initialisation ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestFallbackChainInit:
|
|
def test_no_fallback(self):
|
|
agent = _make_agent(fallback_model=None)
|
|
assert agent._fallback_chain == []
|
|
assert agent._fallback_index == 0
|
|
assert agent._fallback_model is None
|
|
|
|
def test_single_dict_backwards_compat(self):
|
|
fb = {"provider": "openai", "model": "gpt-4o"}
|
|
agent = _make_agent(fallback_model=fb)
|
|
assert agent._fallback_chain == [fb]
|
|
assert agent._fallback_model == fb
|
|
|
|
def test_list_of_providers(self):
|
|
fbs = [
|
|
{"provider": "openai", "model": "gpt-4o"},
|
|
{"provider": "zai", "model": "glm-4.7"},
|
|
]
|
|
agent = _make_agent(fallback_model=fbs)
|
|
assert len(agent._fallback_chain) == 2
|
|
assert agent._fallback_model == fbs[0]
|
|
|
|
def test_invalid_entries_filtered(self):
|
|
fbs = [
|
|
{"provider": "openai", "model": "gpt-4o"},
|
|
{"provider": "", "model": "glm-4.7"},
|
|
{"provider": "zai"},
|
|
"not-a-dict",
|
|
]
|
|
agent = _make_agent(fallback_model=fbs)
|
|
assert len(agent._fallback_chain) == 1
|
|
assert agent._fallback_chain[0]["provider"] == "openai"
|
|
|
|
def test_empty_list(self):
|
|
agent = _make_agent(fallback_model=[])
|
|
assert agent._fallback_chain == []
|
|
assert agent._fallback_model is None
|
|
|
|
def test_invalid_dict_no_provider(self):
|
|
agent = _make_agent(fallback_model={"model": "gpt-4o"})
|
|
assert agent._fallback_chain == []
|
|
|
|
|
|
# ── Chain advancement ─────────────────────────────────────────────────────
|
|
|
|
|
|
class TestFallbackChainAdvancement:
|
|
def test_exhausted_returns_false(self):
|
|
agent = _make_agent(fallback_model=None)
|
|
assert agent._try_activate_fallback() is False
|
|
|
|
def test_advances_index(self):
|
|
fbs = [
|
|
{"provider": "openai", "model": "gpt-4o"},
|
|
{"provider": "zai", "model": "glm-4.7"},
|
|
]
|
|
agent = _make_agent(fallback_model=fbs)
|
|
with patch("agent.auxiliary_client.resolve_provider_client",
|
|
return_value=(_mock_client(), "gpt-4o")):
|
|
assert agent._try_activate_fallback() is True
|
|
assert agent._fallback_index == 1
|
|
assert agent.model == "gpt-4o"
|
|
assert agent._fallback_activated is True
|
|
|
|
def test_second_fallback_works(self):
|
|
fbs = [
|
|
{"provider": "openai", "model": "gpt-4o"},
|
|
{"provider": "zai", "model": "glm-4.7"},
|
|
]
|
|
agent = _make_agent(fallback_model=fbs)
|
|
with patch("agent.auxiliary_client.resolve_provider_client",
|
|
return_value=(_mock_client(), "resolved")):
|
|
assert agent._try_activate_fallback() is True
|
|
assert agent.model == "gpt-4o"
|
|
assert agent._try_activate_fallback() is True
|
|
assert agent.model == "glm-4.7"
|
|
assert agent._fallback_index == 2
|
|
|
|
def test_all_exhausted_returns_false(self):
|
|
fbs = [{"provider": "openai", "model": "gpt-4o"}]
|
|
agent = _make_agent(fallback_model=fbs)
|
|
with patch("agent.auxiliary_client.resolve_provider_client",
|
|
return_value=(_mock_client(), "gpt-4o")):
|
|
assert agent._try_activate_fallback() is True
|
|
assert agent._try_activate_fallback() is False
|
|
|
|
def test_skips_unconfigured_provider_to_next(self):
|
|
"""If resolve_provider_client returns None, skip to next in chain."""
|
|
fbs = [
|
|
{"provider": "broken", "model": "nope"},
|
|
{"provider": "openai", "model": "gpt-4o"},
|
|
]
|
|
agent = _make_agent(fallback_model=fbs)
|
|
with patch("agent.auxiliary_client.resolve_provider_client") as mock_rpc:
|
|
mock_rpc.side_effect = [
|
|
(None, None), # broken provider
|
|
(_mock_client(), "gpt-4o"), # fallback succeeds
|
|
]
|
|
assert agent._try_activate_fallback() is True
|
|
assert agent.model == "gpt-4o"
|
|
assert agent._fallback_index == 2
|
|
|
|
def test_skips_provider_that_raises_to_next(self):
|
|
"""If resolve_provider_client raises, skip to next in chain."""
|
|
fbs = [
|
|
{"provider": "broken", "model": "nope"},
|
|
{"provider": "openai", "model": "gpt-4o"},
|
|
]
|
|
agent = _make_agent(fallback_model=fbs)
|
|
with patch("agent.auxiliary_client.resolve_provider_client") as mock_rpc:
|
|
mock_rpc.side_effect = [
|
|
RuntimeError("auth failed"),
|
|
(_mock_client(), "gpt-4o"),
|
|
]
|
|
assert agent._try_activate_fallback() is True
|
|
assert agent.model == "gpt-4o"
|