mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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).
126 lines
5.3 KiB
Python
126 lines
5.3 KiB
Python
"""Regression tests for the generic unsupported-parameter detector in
|
|
``agent.auxiliary_client``.
|
|
|
|
The original temperature-specific detector (PR #15621) was generalized so the
|
|
same reactive-retry strategy covers any provider that rejects an arbitrary
|
|
request parameter — ``max_tokens``, ``seed``, ``top_p``, future quirks — not
|
|
just ``temperature``. Credit @nicholasrae (PR #15416) for the generalization
|
|
pattern.
|
|
|
|
These tests lock in:
|
|
* ``_is_unsupported_parameter_error(exc, param)`` across common phrasings
|
|
* the back-compat wrapper ``_is_unsupported_temperature_error`` still works
|
|
* the max_tokens retry branch no longer pops a key that was never set
|
|
(``max_tokens is None`` gate)
|
|
* the max_tokens retry branch matches via the generic helper on top of the
|
|
legacy ``"max_tokens"`` / ``"unsupported_parameter"`` substring checks
|
|
"""
|
|
|
|
from unittest.mock import patch, MagicMock, AsyncMock
|
|
|
|
import pytest
|
|
|
|
from agent.auxiliary_client import (
|
|
call_llm,
|
|
async_call_llm,
|
|
_is_unsupported_parameter_error,
|
|
_is_unsupported_temperature_error,
|
|
)
|
|
|
|
|
|
class TestIsUnsupportedParameterError:
|
|
"""The generic detector must match real provider phrasings for any param."""
|
|
|
|
@pytest.mark.parametrize("param,message", [
|
|
# temperature phrasings (regression coverage via the generic API)
|
|
("temperature", "HTTP 400: Unsupported parameter: temperature"),
|
|
("temperature", "Error code: 400 - {'error': {'code': 'unsupported_parameter', 'param': 'temperature'}}"),
|
|
("temperature", "this model does not support temperature"),
|
|
# max_tokens phrasings
|
|
("max_tokens", "HTTP 400: Unsupported parameter: max_tokens"),
|
|
("max_tokens", "Unknown parameter: max_tokens — use max_completion_tokens"),
|
|
("max_tokens", "Invalid parameter: max_tokens is not supported"),
|
|
# arbitrary future params
|
|
("seed", "HTTP 400: unrecognized parameter: seed"),
|
|
("top_p", "Error: top_p is not supported for this model"),
|
|
])
|
|
def test_matches_real_provider_messages(self, param, message):
|
|
assert _is_unsupported_parameter_error(RuntimeError(message), param) is True
|
|
|
|
|
|
|
|
def test_temperature_wrapper_delegates_to_generic(self):
|
|
"""Back-compat: ``_is_unsupported_temperature_error`` still routes through."""
|
|
msg = "HTTP 400: Unsupported parameter: temperature"
|
|
assert _is_unsupported_temperature_error(RuntimeError(msg)) is True
|
|
# And the unrelated-case still holds
|
|
assert _is_unsupported_temperature_error(
|
|
RuntimeError("max_tokens is too large")) is False
|
|
|
|
|
|
def _dummy_response():
|
|
"""Sentinel — real code calls ``_validate_llm_response`` which we patch out."""
|
|
return {"ok": True}
|
|
|
|
|
|
class TestMaxTokensRetryHardening:
|
|
"""The max_tokens retry branch now (a) gates on ``max_tokens is not None``
|
|
and (b) also matches the generic phrasings via the helper.
|
|
"""
|
|
|
|
def test_sync_max_tokens_retry_skipped_when_max_tokens_is_none(self):
|
|
"""No max_tokens kwarg → must not pop/retry even if the error mentions it.
|
|
|
|
Before the hardening, ``kwargs.pop("max_tokens", None)`` was safe but
|
|
``kwargs["max_completion_tokens"] = max_tokens`` would set a None
|
|
value and hit the provider again. The gate skips the whole branch.
|
|
"""
|
|
client = MagicMock()
|
|
client.base_url = "https://api.openai.com/v1"
|
|
err = RuntimeError("HTTP 400: Unsupported parameter: max_tokens")
|
|
client.chat.completions.create.side_effect = err
|
|
|
|
with (
|
|
patch("agent.auxiliary_client._resolve_task_provider_model",
|
|
return_value=("openai-codex", "gpt-5.5", None, None, None)),
|
|
patch("agent.auxiliary_client._get_cached_client",
|
|
return_value=(client, "gpt-5.5")),
|
|
patch("agent.auxiliary_client._validate_llm_response",
|
|
side_effect=lambda resp, _task, **_kw: resp),
|
|
):
|
|
with pytest.raises(RuntimeError):
|
|
call_llm(
|
|
task="session_search",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
temperature=0.3,
|
|
# max_tokens omitted on purpose
|
|
)
|
|
|
|
# Only the initial attempt — no retry because the gate blocked it
|
|
assert client.chat.completions.create.call_count == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_max_tokens_retry_skipped_when_max_tokens_is_none(self):
|
|
client = MagicMock()
|
|
client.base_url = "https://api.openai.com/v1"
|
|
err = RuntimeError("HTTP 400: Unsupported parameter: max_tokens")
|
|
client.chat.completions.create = AsyncMock(side_effect=err)
|
|
|
|
with (
|
|
patch("agent.auxiliary_client._resolve_task_provider_model",
|
|
return_value=("openai-codex", "gpt-5.5", None, None, None)),
|
|
patch("agent.auxiliary_client._get_cached_client",
|
|
return_value=(client, "gpt-5.5")),
|
|
patch("agent.auxiliary_client._validate_llm_response",
|
|
side_effect=lambda resp, _task, **_kw: resp),
|
|
):
|
|
with pytest.raises(RuntimeError):
|
|
await async_call_llm(
|
|
task="session_search",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
temperature=0.3,
|
|
)
|
|
|
|
assert client.chat.completions.create.call_count == 1
|
|
|