import { describe, expect, it } from "bun:test";
import { extractRetryHint } from "@oh-my-pi/pi-utils";
const FAIL_FAST_RE = /quota|exhausted/i;
const shouldFailFast = (errorText: string) => FAIL_FAST_RE.test(errorText);
describe("google-gemini-cli 429 fail-fast detection", () => {
it("fails fast on 'Quota exceeded' messages", () => {
expect(shouldFailFast("Quota exceeded for project")).toBe(true);
});
it("fails fast on 'exhausted' messages", () => {
expect(shouldFailFast("Resource has been exhausted")).toBe(true);
});
it("does not fail fast on ambiguous 429 ('Please retry in 5s')", () => {
expect(shouldFailFast("Please retry in 5s")).toBe(false);
});
it("does not fail fast on generic rate-limit text", () => {
expect(shouldFailFast("Rate limit exceeded, please slow down")).toBe(false);
});
it("matches case-insensitively", () => {
expect(shouldFailFast("QUOTA EXCEEDED")).toBe(true);
expect(shouldFailFast("Resource Has Been Exhausted")).toBe(true);
});
it("does not fail fast on empty error", () => {
expect(shouldFailFast("")).toBe(false);
});
});
describe("extractRetryHint – header parsing", () => {
it("reads retry-after header as seconds", () => {
const headers = new Headers({ "retry-after": "5" });
expect(extractRetryHint(headers)).toBe(5_000);
});
it("reads x-ratelimit-reset-after header as seconds", () => {
const headers = new Headers({ "x-ratelimit-reset-after": "30" });
expect(extractRetryHint(headers)).toBe(30_000);
});
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" });
expect(extractRetryHint(headers)).toBe(5_000);
});
});
describe("extractRetryHint – body text parsing", () => {
it("parses 'retryDelay' JSON field in seconds", () => {
expect(extractRetryHint(undefined, '"retryDelay": "3s"')).toBe(3_000);
});
it("parses 'retryDelay' JSON field in milliseconds", () => {
expect(extractRetryHint(undefined, '"retryDelay": "500ms"')).toBe(500);
});
it("parses 'Please retry in Xs' pattern", () => {
expect(extractRetryHint(undefined, "Please retry in 5s")).toBe(5_000);
});
it("parses 'quota will reset after Xs' simple duration", () => {
expect(extractRetryHint(undefined, "Your quota will reset after 39s")).toBe(39_000);
});
it("parses compound duration 'reset after 1h30m10s'", () => {
expect(extractRetryHint(undefined, "Your quota will reset after 1h30m10s")).toBe(5_410_000);
});
it("parses Codex-style 'try again in Xms'", () => {
expect(extractRetryHint(undefined, "try again in 250ms")).toBe(250);
});
it("parses Codex-style 'try again in Xs'", () => {
expect(extractRetryHint(undefined, "try again in 12s")).toBe(12_000);
});
it("parses Codex 'Try again in ~X min.' (usage_limit_reached friendly text)", () => {
expect(extractRetryHint(undefined, "Try again in ~158 min.")).toBe(158 * 60_000);
});
it("parses 'try again in X min' / 'X minutes' without the tilde", () => {
expect(extractRetryHint(undefined, "try again in 5 min")).toBe(5 * 60_000);
expect(extractRetryHint(undefined, "try again in 90 minutes")).toBe(90 * 60_000);
});
it("parses 'try again in X h' / 'X hour' / 'X hours'", () => {
expect(extractRetryHint(undefined, "try again in 2 h")).toBe(2 * 60 * 60_000);
expect(extractRetryHint(undefined, "try again in 1 hour")).toBe(60 * 60_000);
expect(extractRetryHint(undefined, "try again in 3 hours")).toBe(3 * 60 * 60_000);
});
it("returns undefined when body contains no recognised delay pattern", () => {
expect(extractRetryHint(undefined, "Quota exceeded, please try again later")).toBeUndefined();
});
it("returns undefined for empty error string and no headers", () => {
expect(extractRetryHint(undefined, "")).toBeUndefined();
});
});