Files
hermes-agent/tests/agent/test_init_fallback_refusal_warning.py
kshitijk4poor 13141c6dcb test(agent): make the fake pool's has_available keyword-only like the real one
A positional call would pass the fake but raise TypeError against the real
CredentialPool, which the diagnostic's suppress() would then swallow.
2026-09-24 21:26:28 +05:30

121 lines
3.8 KiB
Python

"""A fully-refusing init-time fallback ladder must say why at WARNING before dying.
One burned primary (an exhausted single-entry OpenRouter pool, for example) plus fallback
entries that cannot resolve credentials raises the generic ``No LLM provider configured``
RuntimeError. Without a WARNING naming each refused entry and its reason, the failure is
indistinguishable from actual config loss in gateway.log.
"""
import logging
from types import SimpleNamespace
import pytest
def _agent(provider="openrouter"):
return SimpleNamespace(
provider=provider,
model="z-ai/glm-5.3-flash",
base_url=None,
api_key=None,
_fallback_activated=False,
)
_LADDER = [
{"provider": "deepseek", "model": "deepseek-v4-pro"},
{"provider": "kimi", "model": "kimi-k3"},
]
_LADDER_REASONS = (
"deepseek",
"no usable credentials",
"kimi",
"kimi auth handshake refused",
)
class _Pool:
def __init__(self, available):
self._available = available
def has_credentials(self):
return True
def has_available(self, *, model=None):
return self._available
@pytest.mark.parametrize(
"primary, ladder, pool_available, raise_match, expected",
[
("openrouter", _LADDER, True, "No LLM provider configured", _LADDER_REASONS),
# Explicit non-OpenRouter primary: provider-specific missing-credentials error.
("anthropic", _LADDER, True, "no API key was found", _LADDER_REASONS),
# #119533: a burned primary pool with no ladder at all must still be named.
("openrouter", [], False, "No LLM provider configured", ("credential pool exhausted",)),
],
)
def test_fully_refusing_ladder_warns_with_each_reason(
monkeypatch, caplog, primary, ladder, pool_available, raise_match, expected
):
"""Primary resolves no client; deepseek resolves none (missing key), kimi raises."""
from agent import agent_init
def _fake_resolve(provider, model=None, **kwargs):
if provider == "kimi":
raise RuntimeError("kimi auth handshake refused")
return (None, None)
monkeypatch.setattr("agent.auxiliary_client.resolve_provider_client", _fake_resolve)
monkeypatch.setattr(
"hermes_cli.fallback_config.resolve_entry_api_key", lambda entry: None
)
monkeypatch.setattr(
"agent.credential_pool.load_pool", lambda provider: _Pool(pool_available)
)
with caplog.at_level(logging.WARNING, logger="run_agent"):
with pytest.raises(RuntimeError, match=raise_match):
agent_init._routed_client_kwargs(_agent(primary), ladder, 60)
warnings = [r for r in caplog.records if r.levelno >= logging.WARNING]
assert len(warnings) == 1
text = warnings[0].getMessage()
assert primary in text
for fragment in expected:
assert fragment in text
def test_recovered_ladder_does_not_warn(monkeypatch, caplog):
"""An entry refusing while a later one serves the init is degraded config, not a failure."""
from agent import agent_init
class _Client:
pass
def _fake_resolve(provider, model=None, **kwargs):
if provider == "kimi":
return (_Client(), model)
return (None, None)
monkeypatch.setattr("agent.auxiliary_client.resolve_provider_client", _fake_resolve)
monkeypatch.setattr(
"hermes_cli.fallback_config.resolve_entry_api_key", lambda entry: None
)
monkeypatch.setattr(
agent_init,
"_client_kwargs_from_routed",
lambda client, timeout: {"api_key": "k"},
)
agent = _agent()
with caplog.at_level(logging.WARNING, logger="run_agent"):
kwargs = agent_init._routed_client_kwargs(agent, _LADDER, 60)
assert kwargs == {"api_key": "k"}
assert agent.provider == "kimi"
assert agent._fallback_activated
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]