import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createEdgeTtsMiddleware, type EdgeTtsMiddlewareOptions, type EdgeTtsSynthesizer } from "./edge-tts-service"; const servers: ReturnType[] = []; afterEach(async () => { await Promise.all( servers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve()))) ); }); async function startServer(options: EdgeTtsMiddlewareOptions) { const middleware = createEdgeTtsMiddleware(options); const server = createServer((request, response) => { void middleware(request, response, () => { response.statusCode = 404; response.end(); }); }); servers.push(server); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const { port } = server.address() as AddressInfo; return `http://127.0.0.1:${port}`; } function postSpeech(baseUrl: string, body: unknown, headers = true) { return fetch(`${baseUrl}/api/tts/edge`, { method: "POST", ...(headers ? { headers: { "Content-Type": "application/json" } } : {}), body: typeof body === "string" ? body : JSON.stringify(body) }); } describe("Edge TTS adapter", () => { it("returns synthesized MP3 audio with the server-owned voice", async () => { const synthesize = vi.fn(async () => Uint8Array.from([73, 68, 51])); const baseUrl = await startServer({ synthesize, defaultVoice: "zh-CN-XiaoxiaoNeural" }); const response = await postSpeech(baseUrl, { text: " 朗读调度结果 " }); expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe("audio/mpeg"); expect(new Uint8Array(await response.arrayBuffer())).toEqual(Uint8Array.from([73, 68, 51])); expect(synthesize).toHaveBeenCalledWith("朗读调度结果", "zh-CN-XiaoxiaoNeural"); }); it("rejects invalid, unsafe and oversized requests before synthesis", async () => { const synthesize = vi.fn(async () => Uint8Array.from([1])); const baseUrl = await startServer({ synthesize }); expect((await postSpeech(baseUrl, "not-json")).status).toBe(400); expect((await postSpeech(baseUrl, { text: "测试" }, false)).status).toBe(415); expect((await postSpeech(baseUrl, { text: "测试", voice: "unsafe" })).status).toBe(400); expect((await postSpeech(baseUrl, { text: "排".repeat(521) })).status).toBe(413); expect(synthesize).not.toHaveBeenCalled(); }); it("limits requests by client and releases the window", async () => { let currentTime = 1_000; const synthesize = vi.fn(async () => Uint8Array.from([1])); const baseUrl = await startServer({ synthesize, maxRequestsPerWindow: 1, rateWindowMs: 1_000, now: () => currentTime }); expect((await postSpeech(baseUrl, { text: "第一次" })).status).toBe(200); expect((await postSpeech(baseUrl, { text: "第二次" })).status).toBe(429); currentTime += 1_000; expect((await postSpeech(baseUrl, { text: "新窗口" })).status).toBe(200); }); it("caps concurrent synthesis work", async () => { let releaseFirst: ((audio: Uint8Array) => void) | undefined; const firstAudio = new Promise((resolve) => { releaseFirst = resolve; }); const synthesize = vi.fn(() => firstAudio); const baseUrl = await startServer({ synthesize, maxConcurrent: 1 }); const firstRequest = postSpeech(baseUrl, { text: "第一段" }); await vi.waitFor(() => expect(synthesize).toHaveBeenCalledTimes(1)); expect((await postSpeech(baseUrl, { text: "第二段" })).status).toBe(429); releaseFirst?.(Uint8Array.from([1])); expect((await firstRequest).status).toBe(200); }); it("returns a gateway timeout without releasing the occupied slot early", async () => { const synthesize = vi.fn(() => new Promise(() => undefined)); const baseUrl = await startServer({ synthesize, timeoutMs: 5 }); expect((await postSpeech(baseUrl, { text: "超时测试" })).status).toBe(504); }); });