emma-hermes/tests/agent/test_auxiliary_transient_re...

83 lines
3.2 KiB
Python

"""Transient-transport retry count + per-model client-cache isolation.
Two related hardening behaviors for auxiliary calls (which include MoA
reference advisors, a pinned-model path where provider fallback is not a
meaningful recovery):
1. A transient transport blip (connection reset / timeout / 5xx) is retried
on the SAME provider several times with backoff before giving up — a single
upstream blip should not silently lose a pinned auxiliary call (root of the
run2 double-advisor "Connection error" collapse).
2. Two auxiliary calls to the same provider/base_url/key but DIFFERENT models
get DISTINCT client-cache keys, so a concurrent fan-out (e.g. opus + gpt-5.5
advisors) never shares one client entry.
"""
from __future__ import annotations
import os
import types
from unittest.mock import patch
import pytest
class _ConnErr(Exception):
"""Stand-in that the transient detector recognizes as a connection blip."""
def test_transient_retry_count_default(monkeypatch):
from agent import auxiliary_client as ac
# No config value -> default.
monkeypatch.setattr(ac, "load_config", lambda: {}, raising=False)
with patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.config.cfg_get", return_value=None):
assert ac._transient_retry_count() == ac._DEFAULT_TRANSIENT_RETRIES
def test_transient_retry_count_configurable_and_clamped():
from agent import auxiliary_client as ac
with patch("hermes_cli.config.cfg_get", return_value=4):
assert ac._transient_retry_count() == 4
with patch("hermes_cli.config.cfg_get", return_value=100):
assert ac._transient_retry_count() == 6 # clamped high
with patch("hermes_cli.config.cfg_get", return_value=-3):
assert ac._transient_retry_count() == 0 # clamped low
with patch("hermes_cli.config.cfg_get", side_effect=RuntimeError):
assert ac._transient_retry_count() == ac._DEFAULT_TRANSIENT_RETRIES
def test_model_participates_in_client_cache_key():
"""Same provider/base_url/key, different model -> different cache key.
This is what stops two concurrent advisors from sharing (and racing on)
one cached client entry."""
from agent.auxiliary_client import _client_cache_key
k_opus = _client_cache_key(
"openrouter", async_mode=False, base_url="https://openrouter.ai/api/v1",
api_key="K", model="anthropic/claude-opus-4.8",
)
k_gpt = _client_cache_key(
"openrouter", async_mode=False, base_url="https://openrouter.ai/api/v1",
api_key="K", model="openai/gpt-5.5",
)
assert k_opus != k_gpt
# Same model still collides (cache still works for reuse).
k_opus2 = _client_cache_key(
"openrouter", async_mode=False, base_url="https://openrouter.ai/api/v1",
api_key="K", model="anthropic/claude-opus-4.8",
)
assert k_opus == k_opus2
def test_missing_model_key_is_stable():
"""Omitting model (legacy callers) is still a valid, stable key."""
from agent.auxiliary_client import _client_cache_key
a = _client_cache_key("openrouter", async_mode=False, base_url="u", api_key="k")
b = _client_cache_key("openrouter", async_mode=False, base_url="u", api_key="k")
assert a == b