





















@@ -28,10 +28,11 @@ beforeAll(async () => {
2828installMinimaxProviderHttpMockCleanup();
29293030function mockMusicGenerationResponse(json: Record<string, unknown>): void {
31+const response = new Response(JSON.stringify(json), {
32+headers: { "content-type": "application/json" },
33+});
3134postJsonRequestMock.mockResolvedValue({
32-response: {
33-json: async () => json,
34-},
35+ response,
3536release: vi.fn(async () => {}),
3637});
3738fetchWithTimeoutMock.mockResolvedValue({
@@ -53,12 +54,22 @@ describe("minimax music generation provider", () => {
5354expectExplicitMusicGenerationCapabilities(buildMinimaxMusicGenerationProvider());
5455});
555656-it("creates music and downloads the generated track", async () => {
57-mockMusicGenerationResponse({
58-task_id: "task-123",
59-audio_url: "https://example.com/out.mp3",
60-lyrics: "our city wakes",
61-base_resp: { status_code: 0 },
57+it("streams generated music chunks from MiniMax", async () => {
58+const chunkA = Buffer.from("ID3\x04\x00mp3-a");
59+const chunkB = Buffer.from("mp3-b");
60+postJsonRequestMock.mockResolvedValue({
61+response: new Response(
62+[
63+`data: ${JSON.stringify({ data: { status: 1, audio: chunkA.toString("hex") }, base_resp: { status_code: 0 } })}`,
64+`data: ${JSON.stringify({ data: { status: 1, audio: chunkB.toString("hex") }, base_resp: { status_code: 0 } })}`,
65+`data: ${JSON.stringify({ data: { status: 2, audio: Buffer.concat([chunkA, chunkB]).toString("hex") }, base_resp: { status_code: 0 } })}`,
66+"",
67+].join("\n\n"),
68+{
69+headers: { "content-type": "text/event-stream" },
70+},
71+),
72+release: vi.fn(async () => {}),
6273});
63746475const provider = buildMinimaxMusicGenerationProvider();
@@ -79,24 +90,81 @@ describe("minimax music generation provider", () => {
7990expect(body.prompt).not.toContain("Target duration");
8091expect(body).not.toHaveProperty("duration");
8192expect(body.lyrics).toBe("our city wakes");
82-expect(body.output_format).toBe("url");
93+expect(body.stream).toBe(true);
94+expect(body.output_format).toBe("hex");
8395expect(body.audio_setting).toEqual({
8496sample_rate: 44100,
8597bitrate: 256000,
8698format: "mp3",
8799});
100+expect(request.timeoutMs).toBe(300000);
88101expect(request?.headers).toBeInstanceOf(Headers);
89102const headers = request?.headers as Headers | undefined;
90103expect(headers?.get("content-type")).toBe("application/json");
91104expect(result.tracks).toHaveLength(1);
92-expect(result.lyrics).toEqual(["our city wakes"]);
93-expect(result.metadata?.taskId).toBe("task-123");
94-expect(result.metadata?.audioUrl).toBe("https://example.com/out.mp3");
105+expect(result.tracks[0]?.buffer).toEqual(Buffer.concat([chunkA, chunkB]));
106+expect(result.tracks[0]?.mimeType).toBe("audio/mpeg");
107+expect(result.metadata?.requestedLyrics).toBe(true);
95108expect(result.metadata).not.toHaveProperty("requestedDurationSeconds");
96109});
97110111+it("reports streaming music task failures", async () => {
112+postJsonRequestMock.mockResolvedValue({
113+response: new Response(
114+`data: ${JSON.stringify({
115+ base_resp: { status_code: 0 },
116+ })}\n\ndata: ${JSON.stringify({
117+ base_resp: { status_code: 2013, status_msg: "render rejected" },
118+ })}`,
119+{
120+headers: { "content-type": "text/event-stream" },
121+},
122+),
123+release: vi.fn(async () => {}),
124+});
125+126+const provider = buildMinimaxMusicGenerationProvider();
127+128+await expect(
129+provider.generateMusic({
130+provider: "minimax",
131+model: "music-2.6",
132+prompt: "upbeat dance-pop with female vocals",
133+cfg: {},
134+}),
135+).rejects.toThrow("MiniMax music generation failed (2013): render rejected");
136+});
137+138+it("keeps terminal streaming audio when no progressive chunks were sent", async () => {
139+const terminalAudio = Buffer.from("terminal-mp3");
140+postJsonRequestMock.mockResolvedValue({
141+response: new Response(
142+`data: ${JSON.stringify({
143+ data: { status: 2, audio: terminalAudio.toString("hex") },
144+ base_resp: { status_code: 0 },
145+ })}`,
146+{
147+headers: { "content-type": "text/event-stream" },
148+},
149+),
150+release: vi.fn(async () => {}),
151+});
152+153+const provider = buildMinimaxMusicGenerationProvider();
154+const result = await provider.generateMusic({
155+provider: "minimax",
156+model: "music-2.6",
157+prompt: "upbeat dance-pop with female vocals",
158+cfg: {},
159+});
160+161+expect(result.tracks[0]?.buffer).toEqual(terminalAudio);
162+});
163+98164it("downloads tracks when url output is returned in data.audio", async () => {
99165mockMusicGenerationResponse({
166+task_id: "task-url",
167+lyrics: "our city wakes",
100168data: {
101169audio: "https://example.com/url-audio.mp3",
102170},
@@ -119,6 +187,90 @@ describe("minimax music generation provider", () => {
119187fetch,
120188);
121189expect(result.tracks[0]?.buffer.byteLength).toBeGreaterThan(0);
190+expect(result.lyrics).toEqual(["our city wakes"]);
191+expect(result.metadata?.taskId).toBe("task-url");
192+expect(result.metadata?.audioUrl).toBe("https://example.com/url-audio.mp3");
193+});
194+195+it("honors explicit long caller timeouts for request and download fallbacks", async () => {
196+mockMusicGenerationResponse({
197+data: {
198+audio: "https://example.com/long-timeout.mp3",
199+},
200+base_resp: { status_code: 0 },
201+});
202+203+const provider = buildMinimaxMusicGenerationProvider();
204+await provider.generateMusic({
205+provider: "minimax",
206+model: "music-2.6",
207+prompt: "upbeat dance-pop with female vocals",
208+cfg: {},
209+lyrics: "our city wakes",
210+timeoutMs: 600000,
211+});
212+213+expect(mockCallArg(postJsonRequestMock).timeoutMs).toBe(600000);
214+expect(fetchWithTimeoutMock).toHaveBeenCalledWith(
215+"https://example.com/long-timeout.mp3",
216+{ method: "GET" },
217+600000,
218+fetch,
219+);
220+});
221+222+it("applies explicit caller timeouts while reading streaming response bodies", async () => {
223+vi.useFakeTimers();
224+try {
225+let cancelled = false;
226+const stream = new ReadableStream<Uint8Array>({
227+start(controller) {
228+setTimeout(() => {
229+if (cancelled) {
230+return;
231+}
232+controller.enqueue(
233+new TextEncoder().encode(
234+`data: ${JSON.stringify({
235+ data: { status: 2, audio: Buffer.from("late-mp3").toString("hex") },
236+ base_resp: { status_code: 0 },
237+ })}`,
238+),
239+);
240+controller.close();
241+}, 200);
242+},
243+cancel() {
244+cancelled = true;
245+},
246+});
247+postJsonRequestMock.mockResolvedValue({
248+response: new Response(stream, {
249+headers: { "content-type": "text/event-stream" },
250+}),
251+release: vi.fn(async () => {}),
252+});
253+254+const provider = buildMinimaxMusicGenerationProvider();
255+const generation = provider.generateMusic({
256+provider: "minimax",
257+model: "music-2.6",
258+prompt: "upbeat dance-pop with female vocals",
259+cfg: {},
260+timeoutMs: 50,
261+});
262+const expectation = expect(generation).rejects.toThrow(
263+"MiniMax music generation timed out after 50ms",
264+);
265+266+await vi.advanceTimersByTimeAsync(0);
267+await vi.advanceTimersByTimeAsync(50);
268+269+await expectation;
270+expect(cancelled).toBe(true);
271+} finally {
272+vi.useRealTimers();
273+}
122274});
123275124276it("rejects instrumental requests that also include lyrics", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。