hermes-agent/tests/agent/test_set_runtime_main_custom_provider.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

231 lines
9.7 KiB
Python

"""Regression test: set_runtime_main() must pass base_url/api_key/api_mode
so that _resolve_auto() can route custom: providers in Step 1.
Fixes https://github.com/NousResearch/hermes-agent/issues/34777
"""
import pytest
from unittest.mock import patch, MagicMock
def _get_globals(mod):
"""Read runtime globals without triggering redaction."""
return {
"provider": mod._RUNTIME_MAIN_PROVIDER,
"model": mod._RUNTIME_MAIN_MODEL,
"base_url": mod._RUNTIME_MAIN_BASE_URL,
"cred": mod._RUNTIME_MAIN_API_KEY, # renamed to avoid redaction
"api_mode": mod._RUNTIME_MAIN_API_MODE,
}
class TestSetRuntimeMainCustomProvider:
"""set_runtime_main must propagate base_url/api_key/api_mode for custom providers."""
def test_clear_resets_all_globals(self):
"""clear_runtime_main resets all five globals to empty."""
import agent.auxiliary_client as mod
mod.set_runtime_main(
"custom:x", "m",
base_url="https://x.example.com",
api_key="sk-abc",
api_mode="chat_completions",
)
mod.clear_runtime_main()
g = _get_globals(mod)
for v in g.values():
assert v == "", f"Expected empty, got {v!r}"
def test_resolve_auto_uses_globals_for_custom_provider(self):
"""_resolve_auto reads base_url/api_key from globals when main_runtime is None."""
import agent.auxiliary_client as mod
mod.clear_runtime_main()
try:
mod.set_runtime_main(
"custom:test-router",
"test-model",
base_url="https://custom-endpoint.example.com/v1",
api_key="sk-test-123",
)
with patch.object(mod, "resolve_provider_client") as mock_resolve:
mock_resolve.return_value = (MagicMock(), "test-model")
client, resolved = mod._resolve_auto(main_runtime=None)
mock_resolve.assert_called_once()
call_args = mock_resolve.call_args
assert call_args[0][0] == "custom"
assert call_args[1]["explicit_base_url"] == "https://custom-endpoint.example.com/v1"
assert call_args[1]["explicit_api_key"] == "sk-test-123"
finally:
mod.clear_runtime_main()
class TestResolveAutoCustomEndToEnd:
"""End-to-end routing assertions — build a *real* client (no mock on
resolve_provider_client) and verify the auxiliary auto-detect chain lands
on the user's custom endpoint instead of falling through to the aggregator
chain. These guard the actual user-visible symptom in #34777 (aux tasks
silently routed to a fallback provider) rather than just the wiring.
"""
@staticmethod
def _client_base_url(client):
for chain in (("base_url",), ("_client", "base_url")):
obj = client
try:
for attr in chain:
obj = getattr(obj, attr)
return str(obj)
except AttributeError:
continue
return None
def test_config_less_custom_endpoint_routes_via_global(self, tmp_path, monkeypatch):
"""custom:<name> with NO config entry: the live base_url carried by
set_runtime_main() must build a real client at that endpoint — not
fall through to Step 2 (the regression in #34777)."""
import agent.auxiliary_client as mod
# Hermetic: no aggregator creds, no stale OPENAI_BASE_URL.
for var in ("OPENROUTER_API_KEY", "NOUS_API_KEY", "OPENAI_API_KEY",
"OPENAI_BASE_URL"):
monkeypatch.delenv(var, raising=False)
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"model:\n"
" default: glm-5.1\n"
" provider: 'custom:ephemeral'\n"
" base_url: ''\n"
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
mod.clear_runtime_main()
try:
mod.set_runtime_main(
"custom:ephemeral",
"glm-5.1",
base_url="https://ephemeral.live/v1",
api_key="sk-live",
)
client, resolved = mod.resolve_provider_client("auto", None)
assert client is not None, (
"config-less custom endpoint fell through to Step 2 — "
"the #34777 bug is back"
)
assert resolved == "glm-5.1"
base = self._client_base_url(client)
assert base and base.rstrip("/") == "https://ephemeral.live/v1"
finally:
mod.clear_runtime_main()
def test_named_custom_with_config_entry_still_routes(self, tmp_path, monkeypatch):
"""Regression guard: custom:<name> WITH a custom_providers entry must
still resolve to that entry's endpoint. An earlier competing fix
collapsed the provider to bare ``custom`` before resolution, which
broke the named-custom branch and returned None here."""
import agent.auxiliary_client as mod
for var in ("OPENROUTER_API_KEY", "NOUS_API_KEY", "OPENAI_API_KEY",
"OPENAI_BASE_URL"):
monkeypatch.delenv(var, raising=False)
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"model:\n"
" default: glm-5.1\n"
" provider: 'custom:openclaw'\n"
" base_url: ''\n"
"custom_providers:\n"
" - name: openclaw\n"
" base_url: 'https://withcfg.example/v1'\n"
" model: glm-5.1\n"
" api_key: cfg-key\n"
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# No live base_url carried — resolution must come from config alone,
# via the named-custom branch in resolve_provider_client.
mod.clear_runtime_main()
try:
mod.set_runtime_main("custom:openclaw", "glm-5.1")
client, resolved = mod.resolve_provider_client("auto", None)
assert client is not None
base = self._client_base_url(client)
assert base and base.rstrip("/") == "https://withcfg.example/v1"
finally:
mod.clear_runtime_main()
def test_named_custom_anthropic_messages_keeps_full_name_and_url(
self, tmp_path, monkeypatch):
"""PR #36043: a ``custom:<name>`` main provider whose config entry
declares ``api_mode: anthropic_messages`` must reach the
named-custom-provider arm of resolve_provider_client — NOT the
anonymous-custom arm, whose ``_to_openai_base_url`` rewrite strips a
trailing ``/anthropic`` into ``/v1`` and 404s against proxies like
Palantir Foundry's Anthropic surface. The resulting client must be an
AnthropicAuxiliaryClient pointed at the ORIGINAL /anthropic URL."""
import agent.auxiliary_client as mod
for var in ("OPENROUTER_API_KEY", "NOUS_API_KEY", "OPENAI_API_KEY",
"OPENAI_BASE_URL"):
monkeypatch.delenv(var, raising=False)
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
proxy_base = "https://acme.palantirfoundry.com/api/v2/llm/proxy/anthropic"
(hermes_home / "config.yaml").write_text(
"model:\n"
" default: claude-4-6-opus\n"
" provider: 'custom:palantir'\n"
" base_url: ''\n"
"custom_providers:\n"
" - name: palantir\n"
f" base_url: '{proxy_base}'\n"
" model: claude-4-6-opus\n"
" api_key: foundry-token\n"
" api_mode: anthropic_messages\n"
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
mod.clear_runtime_main()
try:
# The live runtime carries the same base_url the main agent uses —
# the regression collapsed the provider to bare "custom" whenever a
# runtime base_url was present, which routed here through the
# OpenAI-wire /anthropic→/v1 rewrite.
mod.set_runtime_main(
"custom:palantir",
"claude-4-6-opus",
base_url=proxy_base,
api_key="foundry-token",
api_mode="anthropic_messages",
)
client, resolved = mod.resolve_provider_client("auto", None)
assert client is not None, (
"custom:<name> with anthropic_messages entry resolved to None"
)
assert resolved == "claude-4-6-opus"
assert client.__class__.__name__ == "AnthropicAuxiliaryClient", (
f"expected AnthropicAuxiliaryClient, got {client.__class__.__name__}"
" — the custom:<name> main provider was collapsed to the"
" anonymous-custom OpenAI-wire arm (PR #36043 regression)"
)
# The original /anthropic URL must survive — no /v1 rewrite.
assert getattr(client, "base_url", "").rstrip("/") == proxy_base
# Wiring check: _resolve_auto must hand the FULL custom:<name>
# string to resolve_provider_client, with no explicit_base_url
# override (the named arm reads base_url/api_key from config).
with patch.object(mod, "resolve_provider_client") as mock_resolve:
mock_resolve.return_value = (MagicMock(), "claude-4-6-opus")
mod._resolve_auto(main_runtime=None)
mock_resolve.assert_called_once()
assert mock_resolve.call_args.args[0] == "custom:palantir"
assert mock_resolve.call_args.kwargs["explicit_base_url"] is None
finally:
mod.clear_runtime_main()