Files
hermes-agent/tests/agent/test_credential_pool_operations.py
T

71 lines
3.2 KiB
Python

"""Durable pool administration and selection invariants."""
import time
from dataclasses import replace
import pytest
from agent.credential_pool import CredentialPool, PooledCredential
from hermes_cli.auth import read_credential_pool, write_credential_pool
def _pool(provider="openrouter", *, exhausted=False):
rows = [PooledCredential(
provider=provider, id=f"row{i}", label=f"account{i}", source="manual",
auth_type="api_key", access_token=f"fixture-{i}", priority=i,
last_status="exhausted" if exhausted else None,
last_status_at=time.time() if exhausted else None,
last_error_code=429 if exhausted else None,
last_error_reset_at=time.time() + 3600 if exhausted else None,
) for i in range(2)]
write_credential_pool(provider, [e.to_dict() for e in rows])
return CredentialPool(provider, rows)
def test_target_reset_preserves_sibling_cooldown():
pool = _pool(exhausted=True)
before = read_credential_pool(pool.provider)
assert pool.reset_status("missing") is None
assert read_credential_pool(pool.provider) == before
assert pool.reset_status("row1").last_status is None
after = {e["id"]: e for e in read_credential_pool(pool.provider)}
assert after["row0"] == before[0]
assert after["row1"].get("last_error_reset_at") is None
assert pool.reset_statuses() == 1
assert all(e.get("last_status") is None for e in read_credential_pool(pool.provider))
@pytest.mark.parametrize("strategy", ["fill_first", "round_robin", "random", "least_used"])
def test_selection_counts_only_returned_selections(strategy):
pool = _pool()
pool._strategy = strategy
selected = [pool.select().id for _ in range(2)]
assert {e.id: e.request_count for e in pool.entries()} == {
e.id: selected.count(e.id) for e in pool.entries()}
pool.reset_status("row0") # existing persistence boundary, not a selection
assert sum(e.get("request_count", 0) for e in read_credential_pool(pool.provider)) == 2
pool._current_id = None
pool.try_refresh_matching() # API key is not refreshable; lookup must not count
assert sum(e.request_count for e in pool.entries()) == 2
assert pool.peek() is not None
assert sum(e.request_count for e in pool.entries()) == 2
def test_priority_persists_contiguous_order_without_clearing_cooldown():
pool = _pool(exhausted=True)
before = {e.id: e.last_error_reset_at for e in pool.entries()}
assert pool.move_entry("row1", -5).priority == 0
assert [e["id"] for e in read_credential_pool(pool.provider)] == ["row1", "row0"]
assert pool.move_entry("row1", 99).priority == 1
assert [(e.id, e.priority) for e in pool.entries()] == [("row0", 0), ("row1", 1)]
assert {e.id: e.last_error_reset_at for e in pool.entries()} == before
snapshot = read_credential_pool(pool.provider)
assert pool.move_entry("missing", 0) is None
assert read_credential_pool(pool.provider) == snapshot
def test_priority_honors_anthropic_manual_first():
pool = _pool("anthropic")
pool._entries[1] = replace(pool._entries[1], source="env:ANTHROPIC_API_KEY")
assert pool.move_entry("row1", 0).priority == 1
assert [e.id for e in pool.entries()] == ["row0", "row1"]