fix(fal): bound music/video generation response reads

Reviewed and accepted after live preflight: mergeable clean, checks passing, no unresolved review threads.
This commit is contained in:
Alix-007 2026-06-29 12:11:14 +08:00 committed by GitHub
parent ca1bc58759
commit ce1217a49c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 54 additions and 54 deletions

View file

@ -58,6 +58,13 @@ function streamedAudioResponse(bytes: string): Response {
);
}
function falMusicJsonResponse(value: unknown) {
return {
response: Response.json(value),
release: vi.fn(async () => {}),
};
}
describe("fal music generation provider", () => {
afterEach(() => {
assertOkOrThrowHttpErrorMock.mockClear();
@ -72,18 +79,15 @@ describe("fal music generation provider", () => {
});
it("submits MiniMax music through fal and downloads the generated track", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
audio: {
url: "https://v3b.fal.media/files/b/kangaroo/out.mp3",
content_type: "audio/mpeg",
file_name: "out.mp3",
},
}),
},
release: vi.fn(async () => {}),
});
postJsonRequestMock.mockResolvedValue(
falMusicJsonResponse({
audio: {
url: "https://v3b.fal.media/files/b/kangaroo/out.mp3",
content_type: "audio/mpeg",
file_name: "out.mp3",
},
}),
);
const fetchMock = vi.fn(
async () =>
new Response(Buffer.from("mp3-bytes"), {
@ -125,17 +129,14 @@ describe("fal music generation provider", () => {
});
it("rejects generated music downloads that exceed the configured media cap", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
audio: {
url: "https://v3b.fal.media/files/b/out.mp3",
content_type: "audio/mpeg",
},
}),
},
release: vi.fn(async () => {}),
});
postJsonRequestMock.mockResolvedValue(
falMusicJsonResponse({
audio: {
url: "https://v3b.fal.media/files/b/out.mp3",
content_type: "audio/mpeg",
},
}),
);
vi.stubGlobal(
"fetch",
vi.fn(async () => streamedAudioResponse("too-large")),
@ -167,16 +168,13 @@ describe("fal music generation provider", () => {
});
it("maps ACE-Step duration and instrumental controls", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
audio: { url: "https://example.com/out.wav", content_type: "audio/wav" },
seed: 42,
tags: "lofi, chill",
}),
},
release: vi.fn(async () => {}),
});
postJsonRequestMock.mockResolvedValue(
falMusicJsonResponse({
audio: { url: "https://example.com/out.wav", content_type: "audio/wav" },
seed: 42,
tags: "lofi, chill",
}),
);
vi.stubGlobal(
"fetch",
vi.fn(
@ -205,14 +203,11 @@ describe("fal music generation provider", () => {
});
it("maps Stable Audio duration controls", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
audio: "https://example.com/stable.wav",
}),
},
release: vi.fn(async () => {}),
});
postJsonRequestMock.mockResolvedValue(
falMusicJsonResponse({
audio: "https://example.com/stable.wav",
}),
);
vi.stubGlobal(
"fetch",
vi.fn(

View file

@ -6,7 +6,11 @@ import {
type MusicGenerationRequest,
} from "openclaw/plugin-sdk/music-generation";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { assertOkOrThrowHttpError, postJsonRequest } from "openclaw/plugin-sdk/provider-http";
import {
assertOkOrThrowHttpError,
postJsonRequest,
readProviderJsonResponse,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveFalHttpRequestConfig } from "./http-config.js";
@ -161,7 +165,7 @@ export function buildFalMusicGenerationProvider(): MusicGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "fal music generation failed");
const payload = await response.json();
const payload = await readProviderJsonResponse<unknown>(response, "fal music generation");
const [candidate] = extractGeneratedMusicFileCandidates(payload);
if (!candidate) {
throw new Error("fal music generation response missing audio output");

View file

@ -37,9 +37,7 @@ describe("fal video generation provider", () => {
function releasedJson(value: unknown) {
return {
response: {
json: async () => value,
},
response: Response.json(value),
release: vi.fn(async () => {}),
};
}
@ -254,11 +252,10 @@ describe("fal video generation provider", () => {
it("wraps non-JSON successful fal submit responses", async () => {
mockFalProviderRuntime();
fetchGuardMock.mockResolvedValueOnce({
response: {
json: async () => {
throw new SyntaxError("Unexpected token < in JSON");
},
},
response: new Response("<html><body>Bad Gateway</body></html>", {
status: 200,
headers: { "content-type": "text/html" },
}),
release: vi.fn(async () => {}),
});

View file

@ -5,6 +5,7 @@ import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import {
assertOkOrThrowHttpError,
createProviderOperationDeadline,
readProviderJsonResponse,
type ProviderOperationDeadline,
} from "openclaw/plugin-sdk/provider-http";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
@ -480,9 +481,12 @@ async function fetchFalJson(params: {
try {
await assertOkOrThrowHttpError(response, params.errorContext);
try {
return await response.json();
} catch {
throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
return await readProviderJsonResponse<unknown>(response, params.errorContext);
} catch (error) {
if (error instanceof Error && error.message.endsWith(": malformed JSON response")) {
throw new Error(FAL_VIDEO_MALFORMED_RESPONSE, { cause: error });
}
throw error;
}
} finally {
await release();