diff --git a/src/utils/audio-stream.ts b/src/utils/audio-stream.ts index a6bc2d7d..fec8c83f 100644 --- a/src/utils/audio-stream.ts +++ b/src/utils/audio-stream.ts @@ -62,7 +62,8 @@ export async function* decodeAudioStream( let receivedAudio = false; for await (const event of parseSSE(response)) { - if (!event.data || event.data === '[DONE]') break; + if (event.data === '[DONE]') break; + if (!event.data) continue; let parsed: AudioPayload; try { diff --git a/test/utils/audio-stream.test.ts b/test/utils/audio-stream.test.ts index 23bdefd4..7a6384b1 100644 --- a/test/utils/audio-stream.test.ts +++ b/test/utils/audio-stream.test.ts @@ -9,6 +9,56 @@ async function collectAudio(response: Response): Promise { } describe('decodeAudioStream', () => { + it('skips an empty SSE event before audio', async () => { + const response = sseResponse([ + { data: '' }, + { data: JSON.stringify({ data: { audio: '4142', status: 2 } }) }, + ]); + + expect((await collectAudio(response)).toString()).toBe('AB'); + }); + + it('does not truncate audio when an empty SSE event arrives between chunks', async () => { + const response = sseResponse([ + { data: JSON.stringify({ data: { audio: '4142', status: 1 } }) }, + { data: '' }, + { data: JSON.stringify({ data: { audio: '4344', status: 2 } }) }, + ]); + + expect((await collectAudio(response)).toString()).toBe('ABCD'); + }); + + it('propagates API errors after an empty SSE event', async () => { + const response = sseResponse([ + { data: JSON.stringify({ data: { audio: '4142', status: 1 } }) }, + { data: '' }, + { data: JSON.stringify({ base_resp: { status_code: 1028, status_msg: 'quota exhausted' } }) }, + ]); + + await expect(collectAudio(response)).rejects.toThrow('Quota exhausted'); + }); + + it('still stops at the DONE marker', async () => { + const response = sseResponse([ + { data: JSON.stringify({ data: { audio: '4142', status: 1 } }) }, + { data: '' }, + { data: '[DONE]' }, + { data: JSON.stringify({ base_resp: { status_code: 1028, status_msg: 'after done' } }) }, + ]); + + expect((await collectAudio(response)).toString()).toBe('AB'); + }); + + it('still stops at the terminal audio status after an empty event', async () => { + const response = sseResponse([ + { data: '' }, + { data: JSON.stringify({ data: { audio: '4142', status: 2 } }) }, + { data: JSON.stringify({ data: { audio: '4344', status: 1 } }) }, + ]); + + expect((await collectAudio(response)).toString()).toBe('AB'); + }); + it('decodes a non-SSE JSON success response instead of returning empty audio', async () => { const audio = await collectAudio(jsonResponse({ data: { audio: '414243', status: 2 },