Files
hermes-agent/tests/agent/test_rate_limit_reset_hint.py
teknium1 19f76d7498 test: purge low-value tests, lane py03 (499 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

93 lines
4.2 KiB
Python

"""A rate-limit retry names the provider's reset window on the status line (#26889).
"Rate limited. Waiting 60s" hides the one fact that decides whether to wait or
switch models: a per-minute throttle and a 13-minute plan window look identical
without it. The hint is derived from the same ``reset_at`` the credential pool
uses, so the relative ``resets_in_seconds`` a Codex/ChatGPT usage-limit body
carries must land there too.
"""
import time
from unittest.mock import MagicMock
import pytest
from agent.agent_runtime_helpers import extract_api_error_context
from agent.turn_recovery import compute_error_backoff, reset_hint
def _codex_429(**fields):
err = Exception("HTTP 429: The usage limit has been reached")
err.body = {"error": {"type": "usage_limit_reached", "message": "The usage limit has been reached", **fields}}
return err
def test_relative_resets_in_seconds_becomes_reset_at_when_no_epoch_is_given():
ctx = extract_api_error_context(_codex_429(resets_in_seconds=756))
assert abs(ctx["reset_at"] - (time.time() + 756)) < 5
# An explicit epoch wins over the relative field (same precedence as the credential pool).
ctx = extract_api_error_context(_codex_429(resets_at=1_900_000_000, resets_in_seconds=756))
assert ctx["reset_at"] == 1_900_000_000
# No signal at all → no hint, not a crash.
assert reset_hint(Exception("HTTP 429")) == "" and reset_hint(_codex_429(resets_in_seconds=-5)) == ""
def test_rate_limit_retry_status_names_the_reset_window():
agent = MagicMock()
from agent.status_output import StatusOutputMixin
for name in ("_emit_diagnostic_wait", "_buffer_diagnostic_status"):
setattr(agent, name, getattr(StatusOutputMixin, name).__get__(agent))
agent._client_log_context.return_value = ""
compute_error_backoff(
agent, _codex_429(resets_in_seconds=756), retry_count=1, max_retries=3,
is_rate_limited=True, is_zai_coding_overload=False,
base_url="https://example.test/v1", model="test/model",
)
text = agent._buffer_status.call_args.args[0]
assert "~13m" in text
def test_live_wait_line_names_the_reset_window_too():
"""The buffered line replays only if every retry fails; the line rendered during the wait is
``_emit_diagnostic_wait`` → ``_emit_wait_notice``, and it must carry the same hint."""
agent = MagicMock()
from agent.status_output import StatusOutputMixin
for name in ("_emit_diagnostic_wait", "_buffer_diagnostic_status"):
setattr(agent, name, getattr(StatusOutputMixin, name).__get__(agent))
agent._client_log_context.return_value = ""
def run(err):
compute_error_backoff(
agent, err, retry_count=1, max_retries=3, is_rate_limited=True, is_zai_coding_overload=False,
base_url="https://example.test/v1", model="test/model",
)
return agent._emit_wait_notice.call_args.args[0]
live = run(_codex_429(resets_in_seconds=756))
assert "~13m" in live
# No reset known → no reset window is claimed.
assert "resets in" not in run(Exception("HTTP 429")).lower()
@pytest.mark.parametrize("headers, expected_seconds", [
({"x-ratelimit-reset-requests": "6m0s"}, 360),
({"x-ratelimit-reset-tokens": "1.5s"}, 1.5),
({"x-ratelimit-reset-requests": "20ms"}, 0.02),
({"anthropic-ratelimit-requests-reset": None}, 900), # filled below with now+900 ISO-8601 'Z'
({"retry-after": "30", "x-ratelimit-reset-requests": "6m0s"}, 30), # Retry-After still wins
])
def test_vendor_reset_headers_feed_reset_at(headers, expected_seconds):
from datetime import datetime, timedelta, timezone
if "anthropic-ratelimit-requests-reset" in headers:
when = datetime.now(timezone.utc) + timedelta(seconds=900)
headers["anthropic-ratelimit-requests-reset"] = when.strftime("%Y-%m-%dT%H:%M:%SZ")
err = Exception("HTTP 429: rate limit")
err.response = MagicMock(headers=headers)
assert abs(extract_api_error_context(err)["reset_at"] - (time.time() + expected_seconds)) < 5
# A reset already in the past is ignored rather than reported as "resets in ~0s".
past = Exception("HTTP 429")
past.response = MagicMock(headers={"anthropic-ratelimit-tokens-reset": "2020-01-01T00:00:00Z"})
assert "reset_at" not in extract_api_error_context(past)