fix(minimax): clamp oauth poll interval

This commit is contained in:
Peter Steinberger
2026-05-30 17:24:48 -04:00
parent cdab5fc16a
commit 7eeea30d8c
2 changed files with 158 additions and 2 deletions

View File

@@ -1,7 +1,10 @@
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { loginMiniMaxPortalOAuth, normalizeOAuthExpires } from "./oauth.js";
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
@@ -54,4 +57,152 @@ describe("loginMiniMaxPortalOAuth", () => {
).rejects.toThrow("invalid expired_in");
expect(note).not.toHaveBeenCalled();
});
it("caps oversized authorization poll intervals before scheduling", async () => {
vi.useFakeTimers();
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
let callCount = 0;
const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
callCount += 1;
const body =
init?.body instanceof URLSearchParams
? init.body
: new URLSearchParams(typeof init?.body === "string" ? init.body : "");
if (callCount === 1) {
return new Response(
JSON.stringify({
user_code: "CODE",
verification_uri: "https://example.com/device",
expired_in: Date.now() + MAX_TIMER_TIMEOUT_MS + 10_000,
interval: Number.MAX_SAFE_INTEGER,
state: body.get("state"),
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}
return new Response(
JSON.stringify(
callCount === 2
? { status: "pending" }
: {
status: "success",
access_token: "access",
refresh_token: "refresh",
expired_in: 3600,
},
),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
});
vi.stubGlobal("fetch", fetchMock);
const result = loginMiniMaxPortalOAuth({
openUrl: vi.fn(async () => undefined),
note: vi.fn(async () => undefined),
progress: { update: vi.fn(), stop: vi.fn() },
});
await vi.waitFor(() => {
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
});
await vi.advanceTimersByTimeAsync(MAX_TIMER_TIMEOUT_MS);
await expect(result).resolves.toMatchObject({ access: "access", refresh: "refresh" });
});
it("does not sleep past the authorization expiry deadline", async () => {
vi.useFakeTimers();
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
let callCount = 0;
const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
callCount += 1;
const body =
init?.body instanceof URLSearchParams
? init.body
: new URLSearchParams(typeof init?.body === "string" ? init.body : "");
if (callCount === 1) {
return new Response(
JSON.stringify({
user_code: "CODE",
verification_uri: "https://example.com/device",
expired_in: Date.now() + 10_000,
interval: Number.MAX_SAFE_INTEGER,
state: body.get("state"),
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}
return new Response(JSON.stringify({ status: "pending" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
vi.stubGlobal("fetch", fetchMock);
const result = loginMiniMaxPortalOAuth({
openUrl: vi.fn(async () => undefined),
note: vi.fn(async () => undefined),
progress: { update: vi.fn(), stop: vi.fn() },
});
await vi.waitFor(() => {
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 10_000);
});
const rejection = expect(result).rejects.toThrow("timed out");
await vi.advanceTimersByTimeAsync(10_000);
await rejection;
});
it("keeps the default poll delay for zero authorization intervals", async () => {
vi.useFakeTimers();
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
let callCount = 0;
const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
callCount += 1;
const body =
init?.body instanceof URLSearchParams
? init.body
: new URLSearchParams(typeof init?.body === "string" ? init.body : "");
if (callCount === 1) {
return new Response(
JSON.stringify({
user_code: "CODE",
verification_uri: "https://example.com/device",
expired_in: Date.now() + 10_000,
interval: 0,
state: body.get("state"),
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}
return new Response(
JSON.stringify(
callCount === 2
? { status: "pending" }
: {
status: "success",
access_token: "access",
refresh_token: "refresh",
expired_in: 3600,
},
),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
});
vi.stubGlobal("fetch", fetchMock);
const result = loginMiniMaxPortalOAuth({
openUrl: vi.fn(async () => undefined),
note: vi.fn(async () => undefined),
progress: { update: vi.fn(), stop: vi.fn() },
});
await vi.waitFor(() => {
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 2_000);
});
await vi.advanceTimersByTimeAsync(2_000);
await expect(result).resolves.toMatchObject({ access: "access", refresh: "refresh" });
});
});

View File

@@ -3,6 +3,7 @@ import {
MAX_DATE_TIMESTAMP_MS,
asSafeIntegerInRange,
resolveExpiresAtMsFromDurationOrEpoch,
resolvePositiveTimerTimeoutMs,
} from "openclaw/plugin-sdk/number-runtime";
import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/provider-auth";
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
@@ -259,7 +260,7 @@ export async function loginMiniMaxPortalOAuth(params: {
// Fall back to manual copy/paste if browser open fails.
}
let pollIntervalMs = oauth.interval ? oauth.interval : 2000;
let pollIntervalMs = resolvePositiveTimerTimeoutMs(oauth.interval, 2000);
// The authorization endpoint returns an absolute millisecond deadline.
const expireTimeMs = oauth.expired_in;
@@ -279,7 +280,11 @@ export async function loginMiniMaxPortalOAuth(params: {
throw new Error(result.message);
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
const remainingMs = Math.max(0, expireTimeMs - Date.now());
if (remainingMs <= 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remainingMs)));
pollIntervalMs = Math.max(pollIntervalMs, 2000);
}