Files
oh-my-pi/packages/ai/test/google-gemini-cli-429.test.ts
T

109 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "bun:test";
import { extractRetryDelay } from "@oh-my-pi/pi-ai/providers/google-gemini-cli";
// The fail-fast regex used inside the provider to distinguish "known quota errors" (throw immediately)
// from "ambiguous 429s" (retry up to RATE_LIMIT_BUDGET_MS).
// Option A (minimal): only hard quota limits fail-fast; transient rate-limit messages fall through to retry.
const FAIL_FAST_RE = /quota|exhausted/i;
const shouldFailFast = (errorText: string) => FAIL_FAST_RE.test(errorText);
// normalizeDelay adds a 1 second buffer and rounds up:
// normalizeDelay(ms) = Math.ceil(ms + 1000)
// So all extractRetryDelay results are at least 1 second more than the raw delay.
describe("google-gemini-cli 429 fail-fast detection", () => {
it("fails fast on 'Quota exceeded'", () => {
expect(shouldFailFast("Quota exceeded")).toBe(true);
});
it("fails fast on 'Resource has been exhausted'", () => {
expect(shouldFailFast("Resource has been exhausted")).toBe(true);
});
it("retries (does NOT fail fast) on 'Too many requests'", () => {
expect(shouldFailFast("Too many requests")).toBe(false);
});
it("retries (does NOT fail fast) on 'rate limit exceeded'", () => {
expect(shouldFailFast("rate limit exceeded")).toBe(false);
});
it("fails fast on per-minute quota message", () => {
expect(shouldFailFast("Exceeded per minute quota for generateContent")).toBe(true);
});
it("does NOT fail fast on generic 429 with empty body", () => {
expect(shouldFailFast("")).toBe(false);
});
it("does NOT fail fast on unknown 429 message", () => {
expect(shouldFailFast("Internal server error")).toBe(false);
});
it("is case-insensitive for all variants", () => {
expect(shouldFailFast("QUOTA EXHAUSTED")).toBe(true);
expect(shouldFailFast("Rate Limit Exceeded")).toBe(false);
expect(shouldFailFast("TOO MANY REQUESTS")).toBe(false);
});
});
describe("extractRetryDelay – header parsing", () => {
it("reads retry-after header as seconds → normalizeDelay(5000) = 6000", () => {
const headers = new Headers({ "retry-after": "5" });
// normalizeDelay(5 * 1000) = Math.ceil(5000 + 1000) = 6000
expect(extractRetryDelay("", headers)).toBe(6000);
});
it("reads x-ratelimit-reset-after header as seconds → normalizeDelay(30000) = 31000", () => {
const headers = new Headers({ "x-ratelimit-reset-after": "30" });
// normalizeDelay(30 * 1000) = Math.ceil(30000 + 1000) = 31000
expect(extractRetryDelay("", headers)).toBe(31000);
});
it("prefers retry-after over x-ratelimit-reset-after when both are present", () => {
const headers = new Headers({ "retry-after": "5", "x-ratelimit-reset-after": "30" });
// retry-after is checked first → 6000
expect(extractRetryDelay("", headers)).toBe(6000);
});
});
describe("extractRetryDelay – body text parsing", () => {
it("parses 'retryDelay' JSON field in seconds → normalizeDelay(3000) = 4000", () => {
// Regex: /"retryDelay":\s*"([0-9.]+)(ms|s)"/i
const body = '"retryDelay": "3s"';
// normalizeDelay(3 * 1000) = Math.ceil(3000 + 1000) = 4000
expect(extractRetryDelay(body)).toBe(4000);
});
it("parses 'retryDelay' JSON field in milliseconds → normalizeDelay(500) = 1500", () => {
const body = '"retryDelay": "500ms"';
// normalizeDelay(500) = Math.ceil(500 + 1000) = 1500
expect(extractRetryDelay(body)).toBe(1500);
});
it("parses 'Please retry in Xs' pattern → normalizeDelay(5000) = 6000", () => {
// Regex: /Please retry in ([0-9.]+)(ms|s)/i
expect(extractRetryDelay("Please retry in 5s")).toBe(6000);
});
it("parses 'quota will reset after Xs' simple duration → normalizeDelay(39000) = 40000", () => {
// Regex: /reset after (?:(\d+)h)?(?:(\d+)m)?(\d+(?:\.\d+)?)s/i
// totalMs = 39 * 1000 = 39000 → normalizeDelay = 40000
expect(extractRetryDelay("Your quota will reset after 39s")).toBe(40000);
});
it("parses compound duration 'reset after 1h30m10s'", () => {
// (1*3600 + 30*60 + 10) * 1000 = 5 410 000 ms
// normalizeDelay(5410000) = Math.ceil(5410000 + 1000) = 5411000
expect(extractRetryDelay("Your quota will reset after 1h30m10s")).toBe(5411000);
});
it("returns undefined when body contains no recognised delay pattern", () => {
expect(extractRetryDelay("Quota exceeded, please try again later")).toBeUndefined();
});
it("returns undefined for empty error string and no headers", () => {
expect(extractRetryDelay("")).toBeUndefined();
});
});