import { describe, it, expect, vi, beforeEach } from "vitest"; import { APICallError } from "ai"; const { mockCheckAndIncrement, mockRefund } = vi.hoisted(() => ({ mockCheckAndIncrement: vi.fn(), mockRefund: vi.fn().mockResolvedValue(undefined), })); vi.mock("@/lib/tiers", async () => { const actual = await vi.importActual("@/lib/tiers"); return { ...actual, checkAndIncrementTierLimit: mockCheckAndIncrement, refundAiCall: mockRefund, }; }); const { aiErrorResponse, withAiQuota } = await import("../ai-error"); const { TierLimitError } = await import("@/lib/tiers"); beforeEach(() => { vi.clearAllMocks(); mockCheckAndIncrement.mockResolvedValue(undefined); }); function makeApiCallError(isRetryable: boolean) { return new APICallError({ message: "Provider returned error", url: "https://openrouter.ai/api/v1/responses", requestBodyValues: {}, statusCode: isRetryable ? 503 : 400, isRetryable, }); } describe("aiErrorResponse", () => { it("maps a retryable APICallError to a 503 with a friendly message", async () => { const res = aiErrorResponse(makeApiCallError(true)); expect(res.status).toBe(503); const body = await res.json() as { error: string; retryable: boolean }; expect(body.retryable).toBe(true); expect(body.error).not.toMatch(/openrouter|nvidia|nemotron/i); }); it("maps a non-retryable APICallError to a 502", async () => { const res = aiErrorResponse(makeApiCallError(false)); expect(res.status).toBe(502); const body = await res.json() as { retryable: boolean }; expect(body.retryable).toBe(false); }); it("maps an unknown error to a generic 502", async () => { const res = aiErrorResponse(new Error("boom")); expect(res.status).toBe(502); }); }); describe("withAiQuota", () => { it("returns 403 when the tier limit is already reached, without calling fn", async () => { mockCheckAndIncrement.mockRejectedValue(new TierLimitError("aiCall", "free")); const fn = vi.fn(); const result = await withAiQuota("user-1", "free", fn); expect(result.ok).toBe(false); if (!result.ok) expect(result.response.status).toBe(403); expect(fn).not.toHaveBeenCalled(); }); it("refunds the charged credit when fn throws", async () => { const fn = vi.fn().mockRejectedValue(makeApiCallError(true)); const result = await withAiQuota("user-1", "free", fn); expect(result.ok).toBe(false); expect(mockRefund).toHaveBeenCalledWith("user-1"); if (!result.ok) expect(result.response.status).toBe(503); }); it("returns fn's result and does not refund on success", async () => { const fn = vi.fn().mockResolvedValue({ title: "Pasta" }); const result = await withAiQuota("user-1", "free", fn); expect(result.ok).toBe(true); if (result.ok) expect(result.data).toEqual({ title: "Pasta" }); expect(mockRefund).not.toHaveBeenCalled(); }); });