Files
hermes-agent/tests/hermes_cli/test_oneshot_fallback.py
teknium1 96c9cc9ac6 test: purge low-value tests, lane py12 (287 removed)
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
2026-09-23 03:15:26 -07:00

107 lines
5.3 KiB
Python

"""#81209: ``hermes -z`` must consult the fallback chain at *resolution* time.
A quota-exhausted / expired primary raises ``AuthError`` from ``resolve_runtime_provider`` before
``AIAgent`` exists, so the mid-session ``fallback_model`` wiring never gets a chance. The shared
``resolve_runtime_with_fallback`` helper gives oneshot the gateway's resolution-time behaviour."""
import pytest
from hermes_cli.auth import AuthError
from hermes_cli.runtime_provider import resolve_runtime_with_fallback
_CFG = {"fallback_providers": [
{"provider": "anthropic", "model": "claude-x", "api_key": "fb-key"},
{"provider": "openai", "model": "gpt-x"},
]}
class TestResolveRuntimeWithFallback:
def test_auth_error_walks_chain_in_order_and_re_raises_primary(self, monkeypatch):
calls = []
def fake_resolve(**kw):
calls.append(kw)
if kw.get("requested") == "openai-codex":
raise AuthError("Codex provider quota exhausted (429); retry after 39750s.")
if kw.get("requested") == "anthropic":
raise AuthError("anthropic key missing")
return {"provider": kw["requested"], "api_key": "k"}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", fake_resolve)
runtime, entry = resolve_runtime_with_fallback(_CFG, requested="openai-codex", target_model="gpt-5.4")
assert (runtime["provider"], entry["model"]) == ("openai", "gpt-x")
# Chain walked in config order; the first entry got its inline api_key and its own model.
assert [c.get("requested") for c in calls] == ["openai-codex", "anthropic", "openai"]
assert calls[1]["explicit_api_key"] == "fb-key" and calls[1]["target_model"] == "claude-x"
def all_fail(**kw):
raise AuthError("primary down" if kw.get("requested") == "openai-codex" else "fallback down")
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", all_fail)
with pytest.raises(AuthError, match="primary down"): # primary-error precedence
resolve_runtime_with_fallback(_CFG, requested="openai-codex")
def test_misconfiguration_is_never_rerouted(self, monkeypatch, caplog):
def typo(**kw):
raise ValueError("Unknown provider 'antropic'")
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", typo)
with pytest.raises(ValueError):
resolve_runtime_with_fallback(_CFG, requested="antropic")
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", lambda **kw: {"provider": "p"})
assert resolve_runtime_with_fallback({}) == ({"provider": "p"}, None)
# A misconfigured *fallback* entry is skipped, but loudly: a typo must not vanish at debug level.
def primary_down_first_entry_typo(**kw):
if kw.get("requested") == "openai-codex":
raise AuthError("primary down")
if kw.get("requested") == "anthropic":
raise ValueError("Unknown provider")
return {"provider": kw["requested"]}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", primary_down_first_entry_typo)
with caplog.at_level("WARNING", logger="hermes_cli.runtime_provider"):
_, entry = resolve_runtime_with_fallback(_CFG, requested="openai-codex")
assert entry["provider"] == "openai"
assert any(r.levelname == "WARNING" and "anthropic" in r.getMessage() for r in caplog.records)
def test_run_agent_falls_back_when_primary_resolution_raises_auth_error(monkeypatch):
"""End-to-end: ``_run_agent`` builds AIAgent against the fallback entry's provider/model (#81209)."""
import hermes_cli.oneshot as oneshot_mod
captured = {}
class _FakeAgent:
def __init__(self, **kwargs):
captured.update(kwargs)
def __setattr__(self, name, _value):
pass
def run_conversation(self, _prompt, conversation_history=None):
return {"final_response": "pong", "session_id": "s"}
def close(self):
pass
def fake_resolve(**kw):
# A config-sourced model resolves with requested=None: the ladder reads model.provider itself.
if kw.get("requested") in (None, "openai-codex"):
raise AuthError("Codex provider quota exhausted (429); retry after 39750s. Credentials are still valid.")
return {"api_key": "fb", "base_url": None, "provider": kw["requested"], "api_mode": "chat", "credential_pool": None}
cfg = {"model": {"default": "gpt-5.4", "provider": "openai-codex"}, **_CFG}
monkeypatch.setattr(oneshot_mod, "_create_session_db_for_oneshot", lambda: None)
monkeypatch.setattr("hermes_cli.config.load_config", lambda: cfg)
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", fake_resolve)
monkeypatch.setattr("hermes_cli.tools_config._get_platform_tools", lambda _cfg, _p: [])
monkeypatch.setattr("hermes_cli.mcp_startup.ensure_mcp_discovery_before_agent_build", lambda **_kw: None)
monkeypatch.setattr("run_agent.AIAgent", _FakeAgent)
text, _ = oneshot_mod._run_agent("Health check: reply pong.")
assert text == "pong"
assert (captured["provider"], captured["model"]) == ("anthropic", "claude-x")
assert captured["api_key"] == "fb"