mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
99 lines
3.9 KiB
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));
|
|
}
|
|
}
|