A positional call would pass the fake but raise TypeError against the real CredentialPool, which the diagnostic's suppress() would then swallow.
121 lines
3.8 KiB
Python
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]
|