diff --git a/extensions/minimax/tts.test.ts b/extensions/minimax/tts.test.ts new file mode 100644 index 00000000000..f6aeb049f5b --- /dev/null +++ b/extensions/minimax/tts.test.ts @@ -0,0 +1,48 @@ +import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchWithSsrFGuard: (...args: unknown[]) => fetchWithSsrFGuardMock(...args), + }; +}); + +import { minimaxTTS } from "./tts.js"; + +describe("minimaxTTS", () => { + afterEach(() => { + fetchWithSsrFGuardMock.mockReset(); + vi.restoreAllMocks(); + }); + + it("caps oversized request timeout before arming abort timers", async () => { + const timeoutSpy = vi.spyOn(globalThis, "setTimeout"); + fetchWithSsrFGuardMock.mockResolvedValue({ + response: new Response( + JSON.stringify({ data: { audio: Buffer.from("audio").toString("hex") } }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + release: vi.fn(async () => undefined), + }); + + const audio = await minimaxTTS({ + text: "hello", + apiKey: "sk-test", + baseUrl: "https://api.minimax.io", + model: "speech-2.8-hd", + voiceId: "English_expressive_narrator", + timeoutMs: Number.MAX_SAFE_INTEGER, + }); + + expect(audio.toString()).toBe("audio"); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); + expect(fetchWithSsrFGuardMock.mock.calls[0]?.[0]).toMatchObject({ + timeoutMs: MAX_TIMER_TIMEOUT_MS, + }); + expect(fetchWithSsrFGuardMock.mock.calls[0]?.[0]?.init?.signal).toBeInstanceOf(AbortSignal); + }); +}); diff --git a/extensions/minimax/tts.ts b/extensions/minimax/tts.ts index 22cf850dd5b..15cc4bdbb52 100644 --- a/extensions/minimax/tts.ts +++ b/extensions/minimax/tts.ts @@ -1,3 +1,4 @@ +import { clampTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard, @@ -64,9 +65,10 @@ export async function minimaxTTS(params: { sampleRate = 32000, timeoutMs, } = params; + const safeTimeoutMs = clampTimerTimeoutMs(timeoutMs) ?? 1; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); + const timeout = setTimeout(() => controller.abort(), safeTimeoutMs); try { const { response, release } = await fetchWithSsrFGuard({ @@ -93,7 +95,7 @@ export async function minimaxTTS(params: { }), signal: controller.signal, }, - timeoutMs, + timeoutMs: safeTimeoutMs, policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(baseUrl), auditContext: "minimax.tts", });