mateclaw/mateclaw-server/src/test/java/vip/mate/agent/graph/RetryAfterExtractionTest.java

99 lines
3.9 KiB
Java

package vip.mate.agent.graph;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Parsing of provider-stated retry windows out of 429/529
* response headers, straight from the exception chain (both WebFlux and
* RestClient exception shapes carry their response headers).
*/
class RetryAfterExtractionTest {
private static Throwable ex429(HttpHeaders headers) {
return WebClientResponseException.create(
HttpStatus.TOO_MANY_REQUESTS.value(), "Too Many Requests",
headers, new byte[0], StandardCharsets.UTF_8);
}
@Test
@DisplayName("Retry-After: 7 (delta-seconds) → 7000ms")
void deltaSeconds() {
HttpHeaders h = new HttpHeaders();
h.add("Retry-After", "7");
assertEquals(7_000, NodeStreamingChatHelper.extractRetryAfterMs(ex429(h)));
}
@Test
@DisplayName("Retry-After: HTTP-date → positive delta")
void httpDate() {
HttpHeaders h = new HttpHeaders();
ZonedDateTime future = ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(30);
h.add("Retry-After", DateTimeFormatter.RFC_1123_DATE_TIME.format(future));
long ms = NodeStreamingChatHelper.extractRetryAfterMs(ex429(h));
assertTrue(ms > 25_000 && ms <= 31_000, "expected ≈30s, got " + ms);
}
@Test
@DisplayName("Anthropic RFC-3339 reset instants → earliest future delta wins")
void anthropicResetInstant() {
HttpHeaders h = new HttpHeaders();
h.add("anthropic-ratelimit-requests-reset", Instant.now().plusSeconds(600).toString());
h.add("anthropic-ratelimit-tokens-reset", Instant.now().plusSeconds(60).toString());
long ms = NodeStreamingChatHelper.extractRetryAfterMs(ex429(h));
assertTrue(ms > 55_000 && ms <= 61_000, "earliest bucket (≈60s) must win, got " + ms);
}
@Test
@DisplayName("OpenAI duration-style x-ratelimit-reset ('6m0s') → 360000ms")
void openaiDurationStyle() {
HttpHeaders h = new HttpHeaders();
h.add("x-ratelimit-reset-requests", "6m0s");
assertEquals(360_000, NodeStreamingChatHelper.extractRetryAfterMs(ex429(h)));
}
@Test
@DisplayName("Sub-second values clamp up to the 1s floor")
void subSecondClampsToFloor() {
HttpHeaders h = new HttpHeaders();
h.add("x-ratelimit-reset-tokens", "120ms");
assertEquals(1_000, NodeStreamingChatHelper.extractRetryAfterMs(ex429(h)));
}
@Test
@DisplayName("Absurdly long Retry-After clamps to the 2h ceiling")
void hugeValueClampsToCeiling() {
HttpHeaders h = new HttpHeaders();
h.add("Retry-After", String.valueOf(7 * 24 * 3600)); // one week
assertEquals(2 * 60 * 60 * 1000L, NodeStreamingChatHelper.extractRetryAfterMs(ex429(h)));
}
@Test
@DisplayName("No usable headers → 0 (no hint)")
void noHeadersNoHint() {
assertEquals(0, NodeStreamingChatHelper.extractRetryAfterMs(ex429(new HttpHeaders())));
assertEquals(0, NodeStreamingChatHelper.extractRetryAfterMs(new RuntimeException("429 plain")));
}
@Test
@DisplayName("Hint is found through a wrapping cause chain")
void foundThroughCauseChain() {
HttpHeaders h = new HttpHeaders();
h.add("Retry-After", "5");
RuntimeException wrapped = new RuntimeException("provider call failed", ex429(h));
assertEquals(5_000, NodeStreamingChatHelper.extractRetryAfterMs(wrapped));
}
}