Files
openclaw/extensions/elevenlabs/speech-provider.test.ts
Alix-007 5219c9353d fix(tts): bound voice-list requests (#102865)
* fix(microsoft): add timeout to voices list request

* fix(tts): bound voice-list requests

Co-authored-by: llagy009 <0668001470@xydigit.com>

---------

Co-authored-by: llagy009 <0668001470@xydigit.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-11 00:55:06 -07:00

281 lines
8.7 KiB
TypeScript

// Elevenlabs tests cover speech provider plugin behavior.
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { isValidElevenLabsVoiceId } from "./shared.js";
import { buildElevenLabsSpeechProvider } from "./speech-provider.js";
const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: async (params: {
url: string;
init?: RequestInit;
timeoutMs?: number;
}): Promise<{ response: Response; release: () => Promise<void> }> => {
fetchWithSsrFGuardMock(params);
return {
response: await globalThis.fetch(params.url, params.init),
release: vi.fn(async () => {}),
};
},
ssrfPolicyFromHttpBaseUrlAllowedHostname: () => undefined,
}));
function parseRequestBody(init: RequestInit | undefined): Record<string, unknown> {
if (typeof init?.body !== "string") {
throw new Error("expected string request body");
}
const body: unknown = JSON.parse(init.body);
if (!body || typeof body !== "object" || Array.isArray(body)) {
throw new Error("expected ElevenLabs request body");
}
return body as Record<string, unknown>;
}
describe("elevenlabs speech provider", () => {
const originalFetch = globalThis.fetch;
afterAll(() => {
vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime");
vi.resetModules();
});
afterEach(() => {
globalThis.fetch = originalFetch;
fetchWithSsrFGuardMock.mockClear();
vi.restoreAllMocks();
});
it("exposes the current ElevenLabs TTS model catalog", () => {
const provider = buildElevenLabsSpeechProvider();
expect(provider.models).toEqual([
"eleven_v3",
"eleven_multilingual_v2",
"eleven_flash_v2_5",
"eleven_flash_v2",
"eleven_turbo_v2_5",
"eleven_monolingual_v1",
]);
});
it("forwards the core-resolved voice-list timeout", async () => {
globalThis.fetch = vi.fn(async () => Response.json({ voices: [] })) as unknown as typeof fetch;
const provider = buildElevenLabsSpeechProvider();
await provider.listVoices?.({
providerConfig: { apiKey: "xi-test" },
timeoutMs: 30_000,
});
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
expect.objectContaining({ timeoutMs: 30_000 }),
);
});
it("keeps non-equivalent deprecated ElevenLabs TTS model IDs", async () => {
const provider = buildElevenLabsSpeechProvider();
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
const body = parseRequestBody(init);
expect(body.model_id).toBe("eleven_monolingual_v1");
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
await provider.synthesizeTelephony?.({
text: "hello",
cfg: {} as never,
providerConfig: {
apiKey: "xi-test",
modelId: "eleven_monolingual_v1",
},
timeoutMs: 1_000,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("maps deprecated ElevenLabs TTS model IDs in overrides", async () => {
const provider = buildElevenLabsSpeechProvider();
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
const body = parseRequestBody(init);
expect(body.model_id).toBe("eleven_flash_v2_5");
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
await provider.synthesizeTelephony?.({
text: "hello",
cfg: {} as never,
providerConfig: {
apiKey: "xi-test",
modelId: "eleven_multilingual_v2",
},
providerOverrides: {
modelId: "eleven_turbo_v2_5",
},
timeoutMs: 1_000,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("validates ElevenLabs voice ID length and character rules", () => {
const cases = [
{ value: "pMsXgVXv3BLzUgSXRplE", expected: true },
{ value: "21m00Tcm4TlvDq8ikWAM", expected: true },
{ value: "VoiceAlias1234567890", expected: true },
{ value: "a1b2c3d4e5", expected: true },
{ value: "a".repeat(40), expected: true },
{ value: "", expected: false },
{ value: "abc", expected: false },
{ value: "123456789", expected: false },
{ value: "a".repeat(41), expected: false },
{ value: "a".repeat(100), expected: false },
{ value: "pMsXgVXv3BLz-gSXRplE", expected: false },
{ value: "pMsXgVXv3BLz_gSXRplE", expected: false },
{ value: "pMsXgVXv3BLz gSXRplE", expected: false },
{ value: "../../../etc/passwd", expected: false },
{ value: "voice?param=value", expected: false },
] as const;
for (const testCase of cases) {
expect(isValidElevenLabsVoiceId(testCase.value), testCase.value).toBe(testCase.expected);
}
});
it("applies provider overrides to telephony synthesis", async () => {
const provider = buildElevenLabsSpeechProvider();
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
expect(url).toContain("/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM");
expect(url).toContain("output_format=pcm_22050");
const body = parseRequestBody(init);
expect(body).toEqual({
text: "hello",
model_id: "eleven_v3",
seed: 123,
apply_text_normalization: "on",
language_code: "en",
voice_settings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
speed: 1.2,
},
});
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
const result = await provider.synthesizeTelephony?.({
text: "hello",
cfg: {} as never,
providerConfig: {
apiKey: "xi-test",
voiceId: "pMsXgVXv3BLzUgSXRplE",
modelId: "eleven_multilingual_v2",
},
providerOverrides: {
voiceId: "21m00Tcm4TlvDq8ikWAM",
modelId: "eleven_v3",
seed: 123,
applyTextNormalization: "on",
languageCode: "en",
voiceSettings: {
speed: 1.2,
},
},
timeoutMs: 1_000,
});
expect(result?.outputFormat).toBe("pcm_22050");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("drops out-of-range voice settings before synthesis", async () => {
const provider = buildElevenLabsSpeechProvider();
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
const body = parseRequestBody(init);
expect(body.voice_settings).toEqual({
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
speed: 1,
});
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
await provider.synthesizeTelephony?.({
text: "hello",
cfg: {} as never,
providerConfig: {
apiKey: "xi-test",
voiceSettings: {
stability: -1,
similarityBoost: 2,
style: Number.NaN,
speed: 3,
},
},
providerOverrides: {
voiceSettings: {
speed: 0.1,
},
},
timeoutMs: 1_000,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("drops malformed seed values before synthesis", async () => {
const provider = buildElevenLabsSpeechProvider();
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
const body = parseRequestBody(init);
expect(body).not.toHaveProperty("seed");
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
await provider.synthesizeTelephony?.({
text: "hello",
cfg: {} as never,
providerConfig: {
apiKey: "xi-test",
seed: 1.5,
},
providerOverrides: {
seed: Number.POSITIVE_INFINITY,
},
timeoutMs: 1_000,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("drops malformed latency tier overrides before synthesis", async () => {
const provider = buildElevenLabsSpeechProvider();
const fetchMock = vi.fn(async (url: string) => {
expect(new URL(url).searchParams.has("optimize_streaming_latency")).toBe(false);
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
await provider.synthesize?.({
text: "hello",
target: "audio-file",
cfg: {} as never,
providerConfig: {
apiKey: "xi-test",
},
providerOverrides: {
latencyTier: 2.5,
},
timeoutMs: 1_000,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});