fix(openrouter): preserve completed music when stream cleanup fails (#111056)

* fix(openrouter): preserve completed music when stream cleanup fails

* test(openrouter): clarify stream cleanup authority

* test(openrouter): propagate stream cancellation
This commit is contained in:
Wynne668
2026-07-18 19:49:59 -07:00
committed by GitHub
parent 8365383391
commit 491b87c936
2 changed files with 15 additions and 7 deletions
@@ -41,7 +41,7 @@ vi.mock("openclaw/plugin-sdk/provider-http", async (importOriginal) => {
function sseResponse(
lines: Array<string | Uint8Array>,
options?: { cancel?: () => void; releaseLock?: () => void },
options?: { cancel?: () => void | Promise<void>; releaseLock?: () => void },
): Response {
const encoder = new TextEncoder();
const encodeLine = (line: string | Uint8Array) =>
@@ -59,7 +59,7 @@ function sseResponse(
controller.enqueue(encodeLine(line));
},
cancel() {
options?.cancel?.();
return options?.cancel?.();
},
}),
{ status: 200, headers: { "content-type": "text/event-stream" } },
@@ -211,7 +211,10 @@ describe("openrouter music generation provider", () => {
expect(release).toHaveBeenCalledOnce();
});
it("releases OpenRouter audio stream readers after completion", async () => {
it("preserves completed OpenRouter audio when reader cleanup fails", async () => {
const cancel = vi.fn(async () => {
throw new Error("cancel failed");
});
const releaseLock = vi.fn();
postJsonRequestMock.mockResolvedValue({
response: sseResponse(
@@ -219,7 +222,7 @@ describe("openrouter music generation provider", () => {
audio: Buffer.from("wav-bytes").toString("base64"),
done: true,
}),
{ releaseLock },
{ cancel, releaseLock },
),
release: vi.fn(async () => {}),
});
@@ -234,6 +237,7 @@ describe("openrouter music generation provider", () => {
).resolves.toMatchObject({
tracks: [{ mimeType: "audio/wav" }],
});
expect(cancel).toHaveBeenCalledTimes(1);
expect(releaseLock).toHaveBeenCalledTimes(1);
});
@@ -403,8 +407,10 @@ describe("openrouter music generation provider", () => {
).rejects.toThrow("OpenRouter music generation stream ended before completion");
});
it("surfaces and cancels OpenRouter mid-stream errors", async () => {
const cancel = vi.fn();
it("preserves OpenRouter mid-stream errors when reader cleanup fails", async () => {
const cancel = vi.fn(async () => {
throw new Error("cancel failed");
});
postJsonRequestMock.mockResolvedValue({
response: sseResponse(
[
@@ -295,7 +295,9 @@ async function readOpenRouterAudioStream(
for (const line of lines) {
if (processOpenRouterSseLine(line.trim(), result)) {
flushOpenRouterMusicAudio(result);
await reader.cancel();
// Once [DONE] is observed, the generated result is authoritative.
// Cancellation is cleanup and must not replace it with a transport error.
await reader.cancel().catch(() => {});
return {
audioBuffer: Buffer.concat(result.audioBuffers, result.audioBytes),
transcript: result.transcriptChunks.join(""),