Files
hermes-agent/tests/agent/test_credential_pool_interrupt.py
kshitijk4poor 93a7267b10 test: fold private-predicate and replayed-contract tests into their keepers
Each removed test re-asserts a contract a stronger test already owns at
the real boundary; the keeper is named per group.

- api_server CORS: `_origin_allowed` for an allowlisted origin and for an
  Origin-less client -> the configured-origin preflight test and every
  Origin-less /health request. The Idempotency-Key allow-header assertion
  moves into the preflight test instead of running a second identical
  preflight.
- api_server room grants: the execution-policy predicate's two outcomes ->
  test_api_server_runs' HTTP grant-refresh tests (403 on drift, 200 on the
  authorized digest).
- api_server multimodal: the pure input_image canonicalization -> the
  /v1/responses test that sends the same parts and asserts the same shape.
- credential pool: matched-hint rotation (401) and second-429 rotation ->
  test_credential_pool_routing's real-pool hint test and TestPoolRotationCycle,
  which assert the same steps plus the exact rotate kwargs and swap.
- shared metrics: two runtime cases that replay the send resolver's truth
  table -> TestSendRequiresCollection; the e2e gzip-retry test (both
  attempts compress in the same second, so it cannot see mtime) ->
  TestCompression::test_gzip_is_deterministic_across_time; e2e outbox
  untouched -> TestOutboxIsNotAQueue.
- mcp oauth: provider reuse across reconnects -> the manager restore test;
  the non-tty private predicate -> build_oauth_auth's fail-fast test; the
  manager disk-watch test -> the integration test, which gains its one
  unique assertion (an unchanged file returns False).
- startup watchdog: disarm-without-arm -> test_disarm_is_idempotent, whose
  second disarm runs in the same state and branch.
2026-09-29 15:58:55 +05:30

59 lines
2.2 KiB
Python

"""Regression test for #26145: credential pool rotation after interrupt-resume.
When has_retried_429 is lost (user cancels between 429s), the pool should
still rotate if the current credential is already marked exhausted.
"""
from unittest.mock import MagicMock, patch
from agent.credential_pool import PooledCredential, STATUS_EXHAUSTED
from agent.error_classifier import FailoverReason
def _make_entry(idx, **overrides):
defaults = dict(
provider="test-provider",
id=f"cred-{idx}",
label=f"Credential {idx}",
auth_type="api_key",
priority=idx,
source="manual",
access_token=f"key-{idx}",
)
defaults.update(overrides)
return PooledCredential(**defaults)
def _make_pool(entries):
pool = MagicMock()
pool.entries = MagicMock(return_value=entries)
pool.current.return_value = entries[0]
# Must be set explicitly — MagicMock.provider returns a truthy
# child mock, which would trigger the provider-mismatch guard.
pool.provider = ""
return pool
def test_rotate_immediately_when_credential_already_exhausted():
"""If current credential has last_status='exhausted', rotate on first 429
instead of retrying (Option A fix for #26145)."""
entries = [_make_entry(0, last_status=STATUS_EXHAUSTED, last_error_code=429), _make_entry(1)]
pool = _make_pool(entries)
pool.mark_exhausted_and_rotate.return_value = entries[1]
from run_agent import AIAgent
with patch("model_tools.get_tool_definitions", return_value=[]), patch("model_tools.check_toolset_requirements", return_value={}), patch("agent.process_bootstrap.OpenAI"):
agent = MagicMock(spec=AIAgent)
agent._credential_pool = pool
agent._swap_credential = MagicMock()
recovered, retried = AIAgent._recover_with_credential_pool(
agent,
status_code=429,
has_retried_429=False, # Key: False on first 429 after interrupt
classified_reason=FailoverReason.rate_limit,
)
assert recovered is True
assert retried is False
pool.mark_exhausted_and_rotate.assert_called_once()
agent._swap_credential.assert_called_once_with(entries[1])