
















@@ -0,0 +1,83 @@
1+import { describe, expect, it, vi } from "vitest";
2+import { isRetryableDiscordDeliveryError } from "./delivery-retry.js";
3+import { DiscordError, RateLimitError } from "./internal/discord.js";
4+import { createDiscordRetryRunner, isRetryableDiscordTransientError } from "./retry.js";
5+6+const ZERO_DELAY_RETRY = { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 };
7+8+function createRateLimitError(retryAfter = 0): RateLimitError {
9+const response = new Response(null, {
10+status: 429,
11+headers: {
12+"X-RateLimit-Scope": "user",
13+"X-RateLimit-Bucket": "bucket-1",
14+},
15+});
16+const RateLimitErrorCtor = RateLimitError as unknown as new (
17+response: Response,
18+body: { message: string; retry_after: number; global: boolean },
19+) => RateLimitError;
20+return new RateLimitErrorCtor(response, {
21+message: "rate limited",
22+retry_after: retryAfter,
23+global: false,
24+});
25+}
26+27+describe("isRetryableDiscordTransientError", () => {
28+it.each([
29+["rate limit", createRateLimitError()],
30+["408 status", Object.assign(new Error("request timeout"), { status: 408 })],
31+["502 status", Object.assign(new Error("bad gateway"), { status: 502 })],
32+["503 statusCode", Object.assign(new Error("service unavailable"), { statusCode: 503 })],
33+["fetch failed", new TypeError("fetch failed")],
34+["ECONNRESET", Object.assign(new Error("socket hang up"), { code: "ECONNRESET" })],
35+["ETIMEDOUT cause", new Error("request failed", { cause: { code: "ETIMEDOUT" } })],
36+["abort", Object.assign(new Error("aborted"), { name: "AbortError" })],
37+])("retries %s", (_name, err) => {
38+expect(isRetryableDiscordTransientError(err)).toBe(true);
39+});
40+41+it.each([
42+["400 status", Object.assign(new Error("bad request"), { status: 400 })],
43+["403 status", Object.assign(new Error("missing permissions"), { statusCode: 403 })],
44+["unknown channel", new Error("Unknown Channel")],
45+["plain string", "fetch failed"],
46+])("does not retry %s", (_name, err) => {
47+expect(isRetryableDiscordTransientError(err)).toBe(false);
48+});
49+});
50+51+describe("createDiscordRetryRunner", () => {
52+it("retries transient transport errors", async () => {
53+const fn = vi.fn().mockRejectedValueOnce(new TypeError("fetch failed")).mockResolvedValue("ok");
54+const runner = createDiscordRetryRunner({ retry: ZERO_DELAY_RETRY });
55+56+await expect(runner(fn, "send")).resolves.toBe("ok");
57+expect(fn).toHaveBeenCalledTimes(2);
58+});
59+60+it("stops after configured transient retry attempts", async () => {
61+const fn = vi.fn().mockRejectedValue(new TypeError("fetch failed"));
62+const runner = createDiscordRetryRunner({ retry: ZERO_DELAY_RETRY });
63+64+await expect(runner(fn, "send")).rejects.toThrow("fetch failed");
65+expect(fn).toHaveBeenCalledTimes(2);
66+});
67+});
68+69+describe("isRetryableDiscordDeliveryError", () => {
70+it("retries status-coded errors from injected delivery dependencies", () => {
71+expect(
72+isRetryableDiscordDeliveryError(Object.assign(new Error("bad gateway"), { status: 502 })),
73+).toBe(true);
74+});
75+76+it("does not retry Discord client errors after the request runner handled them", () => {
77+const err = new DiscordError(new Response("upstream", { status: 502 }), {
78+message: "Bad Gateway",
79+});
80+81+expect(isRetryableDiscordDeliveryError(err)).toBe(false);
82+});
83+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。