import { describe, test, expect, mock } from "bun:test"; import { withCwRetry } from "../../src/modules/cw-utils/withCwRetry"; describe("withCwRetry", () => { // ------------------------------------------------------------------- // Successful execution // ------------------------------------------------------------------- test("returns result on first successful call", async () => { const fn = mock(() => Promise.resolve("ok")); const result = await withCwRetry(fn); expect(result).toBe("ok"); expect(fn).toHaveBeenCalledTimes(1); }); test("returns result when call succeeds after transient failure", async () => { let attempt = 0; const fn = mock(() => { attempt++; if (attempt === 1) { const err: any = new Error("timeout"); err.isAxiosError = true; err.code = "ETIMEDOUT"; return Promise.reject(err); } return Promise.resolve("recovered"); }); const result = await withCwRetry(fn, { baseDelayMs: 1 }); expect(result).toBe("recovered"); expect(fn).toHaveBeenCalledTimes(2); }); // ------------------------------------------------------------------- // Retry on transient errors // ------------------------------------------------------------------- test("retries on ECONNABORTED", async () => { let calls = 0; const fn = mock(() => { calls++; if (calls < 3) { const err: any = new Error("aborted"); err.isAxiosError = true; err.code = "ECONNABORTED"; return Promise.reject(err); } return Promise.resolve("done"); }); const result = await withCwRetry(fn, { maxAttempts: 3, baseDelayMs: 1 }); expect(result).toBe("done"); expect(fn).toHaveBeenCalledTimes(3); }); test("retries on ECONNRESET", async () => { let calls = 0; const fn = mock(() => { calls++; if (calls === 1) { const err: any = new Error("reset"); err.isAxiosError = true; err.code = "ECONNRESET"; return Promise.reject(err); } return Promise.resolve("ok"); }); const result = await withCwRetry(fn, { baseDelayMs: 1 }); expect(result).toBe("ok"); }); test("retries on ECONNREFUSED", async () => { let calls = 0; const fn = mock(() => { calls++; if (calls === 1) { const err: any = new Error("refused"); err.isAxiosError = true; err.code = "ECONNREFUSED"; return Promise.reject(err); } return Promise.resolve("ok"); }); const result = await withCwRetry(fn, { baseDelayMs: 1 }); expect(result).toBe("ok"); }); test("retries on ERR_NETWORK", async () => { let calls = 0; const fn = mock(() => { calls++; if (calls === 1) { const err: any = new Error("network"); err.isAxiosError = true; err.code = "ERR_NETWORK"; return Promise.reject(err); } return Promise.resolve("ok"); }); const result = await withCwRetry(fn, { baseDelayMs: 1 }); expect(result).toBe("ok"); }); test("retries on ENETUNREACH", async () => { let calls = 0; const fn = mock(() => { calls++; if (calls === 1) { const err: any = new Error("unreachable"); err.isAxiosError = true; err.code = "ENETUNREACH"; return Promise.reject(err); } return Promise.resolve("ok"); }); const result = await withCwRetry(fn, { baseDelayMs: 1 }); expect(result).toBe("ok"); }); test("retries on 5xx server errors", async () => { let calls = 0; const fn = mock(() => { calls++; if (calls === 1) { const err: any = new Error("server error"); err.isAxiosError = true; err.response = { status: 502 }; return Promise.reject(err); } return Promise.resolve("ok"); }); const result = await withCwRetry(fn, { baseDelayMs: 1 }); expect(result).toBe("ok"); }); test("retries on 500 status", async () => { let calls = 0; const fn = mock(() => { calls++; if (calls === 1) { const err: any = new Error("internal"); err.isAxiosError = true; err.response = { status: 500 }; return Promise.reject(err); } return Promise.resolve("ok"); }); const result = await withCwRetry(fn, { baseDelayMs: 1 }); expect(result).toBe("ok"); }); // ------------------------------------------------------------------- // Non-retryable errors // ------------------------------------------------------------------- test("does not retry on 4xx errors", async () => { const err: any = new Error("not found"); err.isAxiosError = true; err.response = { status: 404 }; const fn = mock(() => Promise.reject(err)); await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(1); }); test("does not retry on 400 errors", async () => { const err: any = new Error("bad request"); err.isAxiosError = true; err.response = { status: 400 }; const fn = mock(() => Promise.reject(err)); await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(1); }); test("does not retry on non-Axios errors", async () => { const fn = mock(() => Promise.reject(new Error("generic"))); await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow( "generic", ); expect(fn).toHaveBeenCalledTimes(1); }); test("does not retry on non-object errors", async () => { const fn = mock(() => Promise.reject("string error")); await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toBe( "string error", ); expect(fn).toHaveBeenCalledTimes(1); }); // ------------------------------------------------------------------- // Max attempts exhausted // ------------------------------------------------------------------- test("throws after maxAttempts exhausted", async () => { const err: any = new Error("timeout"); err.isAxiosError = true; err.code = "ETIMEDOUT"; const fn = mock(() => Promise.reject(err)); await expect( withCwRetry(fn, { maxAttempts: 2, baseDelayMs: 1 }), ).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(2); }); test("throws the last error when retries exhausted", async () => { const err: any = new Error("persistent timeout"); err.isAxiosError = true; err.code = "ETIMEDOUT"; const fn = mock(() => Promise.reject(err)); try { await withCwRetry(fn, { maxAttempts: 3, baseDelayMs: 1 }); expect(true).toBe(false); // should not reach } catch (e: any) { expect(e.message).toBe("persistent timeout"); } }); // ------------------------------------------------------------------- // Options // ------------------------------------------------------------------- test("defaults to 3 maxAttempts", async () => { const err: any = new Error("timeout"); err.isAxiosError = true; err.code = "ETIMEDOUT"; const fn = mock(() => Promise.reject(err)); await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(3); }); test("accepts custom maxAttempts", async () => { const err: any = new Error("timeout"); err.isAxiosError = true; err.code = "ETIMEDOUT"; const fn = mock(() => Promise.reject(err)); await expect( withCwRetry(fn, { maxAttempts: 5, baseDelayMs: 1 }), ).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(5); }); test("maxAttempts of 1 means no retries", async () => { const err: any = new Error("timeout"); err.isAxiosError = true; err.code = "ETIMEDOUT"; const fn = mock(() => Promise.reject(err)); await expect( withCwRetry(fn, { maxAttempts: 1, baseDelayMs: 1 }), ).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(1); }); });