BE-AGT-004 parse exhausted recovery hints

This commit is contained in:
zhi
2026-04-01 04:18:44 +00:00
parent a94ef43974
commit 2cc07b9c3e
2 changed files with 189 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ from app.services.agent_status import (
AgentStatusError,
HEARTBEAT_TIMEOUT_SECONDS,
DEFAULT_RECOVERY_HOURS,
parse_exhausted_recovery_at,
transition_to_busy,
transition_to_idle,
transition_to_offline,
@@ -170,6 +171,55 @@ class TestTransitionToOffline:
assert result.status == AgentStatus.OFFLINE
# ---------------------------------------------------------------------------
# Recovery time parsing
# ---------------------------------------------------------------------------
class TestParseExhaustedRecoveryAt:
def test_parses_retry_after_seconds_header(self):
recovery = parse_exhausted_recovery_at(
now=NOW,
headers={"Retry-After": "120"},
)
assert recovery == NOW + timedelta(seconds=120)
def test_parses_retry_after_http_date_header(self):
recovery = parse_exhausted_recovery_at(
now=NOW,
headers={"Retry-After": "Wed, 01 Apr 2026 12:05:00 GMT"},
)
assert recovery == datetime(2026, 4, 1, 12, 5, 0, tzinfo=timezone.utc)
def test_parses_reset_in_minutes_from_message(self):
recovery = parse_exhausted_recovery_at(
now=NOW,
message="rate limit exceeded, reset in 7 mins",
)
assert recovery == NOW + timedelta(minutes=7)
def test_parses_retry_after_seconds_from_message(self):
recovery = parse_exhausted_recovery_at(
now=NOW,
message="429 too many requests; retry after 45 seconds",
)
assert recovery == NOW + timedelta(seconds=45)
def test_parses_resets_at_iso_timestamp_from_message(self):
recovery = parse_exhausted_recovery_at(
now=NOW,
message="quota exhausted, resets at 2026-04-01T14:30:00Z",
)
assert recovery == datetime(2026, 4, 1, 14, 30, 0, tzinfo=timezone.utc)
def test_falls_back_to_default_when_unparseable(self):
recovery = parse_exhausted_recovery_at(
now=NOW,
headers={"Retry-After": "not-a-date"},
message="please try later maybe soon",
)
assert recovery == NOW + timedelta(hours=DEFAULT_RECOVERY_HOURS)
# ---------------------------------------------------------------------------
# * → Exhausted (API quota)
# ---------------------------------------------------------------------------
@@ -210,6 +260,28 @@ class TestTransitionToExhausted:
)
assert result.status == AgentStatus.EXHAUSTED
def test_parses_recovery_from_headers_when_timestamp_not_explicitly_provided(self, db):
agent = _make_agent(db, status=AgentStatus.BUSY)
result = transition_to_exhausted(
db,
agent,
reason=ExhaustReason.RATE_LIMIT,
headers={"Retry-After": "90"},
now=NOW,
)
assert result.recovery_at == NOW + timedelta(seconds=90)
def test_parses_recovery_from_message_when_timestamp_not_explicitly_provided(self, db):
agent = _make_agent(db, status=AgentStatus.BUSY)
result = transition_to_exhausted(
db,
agent,
reason=ExhaustReason.BILLING,
message="billing quota exhausted, resets at 2026-04-01T15:00:00Z",
now=NOW,
)
assert result.recovery_at == datetime(2026, 4, 1, 15, 0, 0, tzinfo=timezone.utc)
# ---------------------------------------------------------------------------
# Heartbeat timeout check