import { describe, expect, it } from "vitest"; import { canRetryTransport, isRetryableHttpStatus, isRetryableSemantics, jitteredDelay, retryDelayFor, } from "../../src/adapters/http/http-retry-lifecycle.ts"; describe("HTTP retry lifecycle policy", () => { it("allows KEYED replay only before a physical dispatch", () => { expect(canRetryTransport("KEYED", "PREPARING")).toBe(true); expect(canRetryTransport("KEYED", "READY_TO_SEND")).toBe(true); expect(canRetryTransport("KEYED", "DISPATCHED")).toBe(false); expect(canRetryTransport("KEYED", "RESPONSE_HEADERS")).toBe(false); }); it("keeps SAFE and IDEMPOTENT retryable while NEVER is terminal", () => { expect(isRetryableSemantics("SAFE")).toBe(true); expect(isRetryableSemantics("IDEMPOTENT")).toBe(true); expect(isRetryableSemantics("KEYED")).toBe(false); expect(isRetryableSemantics("NEVER")).toBe(false); expect(canRetryTransport("SAFE", "DISPATCHED")).toBe(true); expect(canRetryTransport("IDEMPOTENT", "READING_BODY")).toBe(true); expect(canRetryTransport("NEVER", "PREPARING")).toBe(false); }); it("owns the closed retryable status vocabulary", () => { for (const status of [408, 425, 429, 502, 503, 504]) { expect(isRetryableHttpStatus(status)).toBe(true); } for (const status of [400, 401, 403, 404, 409, 500, 501]) { expect(isRetryableHttpStatus(status)).toBe(false); } }); it("uses bounded full jitter and rejects excessive Retry-After", () => { expect(jitteredDelay(0, () => 0)).toBe(0); expect(jitteredDelay(0, () => 0.5)).toBe(125); expect(jitteredDelay(8, () => 0.5)).toBe(1_000); expect(retryDelayFor(null, 0, () => 0.5)).toBe(125); expect(retryDelayFor(400, 0, () => 0.5)).toBe(400); expect(retryDelayFor(5_001, 0, () => 0.5)).toBeNull(); }); });