Skip to content

Commit 6ac8e0b

Browse files
committed
test: cover mixed retry failure chains
1 parent cb32185 commit 6ac8e0b

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

test/index-retry.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,105 @@ describe("OpenAIAuthPlugin rate-limit retry", () => {
565565
);
566566
});
567567

568+
it("keeps the total request cap when empty-response retries and server-error rotation combine", async () => {
569+
const logger = await import("../lib/logger.js");
570+
const logWarnSpy = vi.spyOn(logger, "logWarn").mockImplementation(() => {});
571+
572+
const accounts = Array.from({ length: 6 }, (_, index) =>
573+
createMockAccount({
574+
index,
575+
accountId: `account-${index + 1}`,
576+
email: `user${index + 1}@example.com`,
577+
refreshToken: `refresh-token-${index + 1}`,
578+
access: `access-token-account-${index + 1}`,
579+
}),
580+
);
581+
accountManagerState.accounts = accounts;
582+
accountManagerState.accountSelections = [...accounts];
583+
584+
const serverErrorResponse = () =>
585+
new Response(
586+
JSON.stringify({
587+
error: { code: "server_error", message: "temporary outage" },
588+
}),
589+
{
590+
status: 500,
591+
headers: { "content-type": "application/json" },
592+
},
593+
);
594+
595+
const fetchMock = vi
596+
.fn()
597+
.mockResolvedValueOnce(
598+
new Response("{}", {
599+
status: 200,
600+
headers: { "content-type": "application/json" },
601+
}),
602+
)
603+
.mockResolvedValueOnce(serverErrorResponse())
604+
.mockResolvedValueOnce(serverErrorResponse())
605+
.mockResolvedValueOnce(serverErrorResponse())
606+
.mockResolvedValueOnce(serverErrorResponse())
607+
.mockResolvedValueOnce(serverErrorResponse());
608+
globalThis.fetch = fetchMock as any;
609+
610+
const { OpenAIAuthPlugin } = await import("../index.js");
611+
const client = {
612+
tui: { showToast: vi.fn() },
613+
auth: { set: vi.fn() },
614+
} as any;
615+
const plugin = await OpenAIAuthPlugin({ client });
616+
617+
const getAuth = async () => ({
618+
type: "oauth" as const,
619+
access: "a",
620+
refresh: "r",
621+
expires: Date.now() + 60_000,
622+
multiAccount: true,
623+
});
624+
625+
const sdk = (await plugin.auth.loader(getAuth, { options: {}, models: {} })) as any;
626+
const fetchPromise = sdk.fetch("https://example.com", {});
627+
628+
await vi.advanceTimersByTimeAsync(1500);
629+
630+
const response = await fetchPromise;
631+
const payload = await response.json();
632+
633+
expect(fetchMock).toHaveBeenCalledTimes(6);
634+
expect(
635+
fetchMock.mock.calls.map(
636+
(call) => (call[1]?.headers as Headers).get("x-account-id"),
637+
),
638+
).toEqual([
639+
"account-1",
640+
"account-1",
641+
"account-2",
642+
"account-3",
643+
"account-4",
644+
"account-5",
645+
]);
646+
expect(response.status).toBe(503);
647+
expect(payload).toEqual({
648+
error: {
649+
message:
650+
"Request attempt budget exhausted after 6 outbound request(s). Try again after the current retries settle.",
651+
},
652+
});
653+
expect(logWarnSpy).toHaveBeenCalledWith(
654+
"Empty response received (attempt 1/2). Retrying...",
655+
);
656+
expect(logWarnSpy).toHaveBeenCalledWith(
657+
"Request attempt budget exhausted.",
658+
expect.objectContaining({
659+
reason: "primary",
660+
accountIndex: 5,
661+
budget: 6,
662+
consumed: 6,
663+
}),
664+
);
665+
});
666+
568667
it("rebuilds request headers after rotating to the next workspace", async () => {
569668
const account = createMockAccount({
570669
workspaces: [

0 commit comments

Comments
 (0)