diff --git a/src/commands/speech/transcribe.ts b/src/commands/speech/transcribe.ts index 5a093273..c9810bb8 100644 --- a/src/commands/speech/transcribe.ts +++ b/src/commands/speech/transcribe.ts @@ -174,7 +174,11 @@ export default defineCommand({ } if (!finished) { - process.stderr.write('[warning] Stream ended before the final event; the transcript may be incomplete.\n'); + if (toStdout) process.stdout.write('\n'); + throw new CLIError( + 'Stream ended before the final event; the transcript may be incomplete.', + ExitCode.GENERAL, + ); } // Assembled by index, per the API's contract — arrival order only shows diff --git a/test/commands/speech/transcribe.test.ts b/test/commands/speech/transcribe.test.ts index d606acfa..3f889630 100644 --- a/test/commands/speech/transcribe.test.ts +++ b/test/commands/speech/transcribe.test.ts @@ -420,11 +420,18 @@ describe('speech transcribe command', () => { } }); - it('warns on stderr when the stream ends without the final event', async () => { + it.each([ + { format: 'text' as const, ending: 'eof' }, + { format: 'text' as const, ending: 'done' }, + { format: 'json' as const, ending: 'eof' }, + { format: 'json' as const, ending: 'done' }, + ])('rejects incomplete $format transcription at $ending', async ({ format, ending }) => { const { dir, filePath } = makeTempAudio(); - const response = sseResponse([ - { data: '{"index":0,"delta":"truncated","finish":false}' }, - ]); + const response = new Response( + 'data: {"index":0,"delta":"truncated","finish":false}\n\n' + + (ending === 'done' ? 'data: [DONE]\n\n' : ''), + { headers: { 'Content-Type': 'text/event-stream' } }, + ); try { await withStubbedFetch( @@ -433,16 +440,18 @@ describe('speech transcribe command', () => { let captured = ''; const stderr = await captureStderr(async () => { captured = await captureStdout(async () => { - await transcribeCommand.execute(baseConfig, { - ...baseFlags, - file: filePath, - stream: true, + await expect(transcribeCommand.execute({ ...baseConfig, output: format }, { + ...baseFlags, file: filePath, stream: true, + })).rejects.toMatchObject({ + name: 'CLIError', + exitCode: 1, + message: 'Stream ended before the final event; the transcript may be incomplete.', }); }); }); - expect(captured).toBe('truncated\n'); - expect(stderr).toContain('Stream ended before the final event'); + expect(captured).toBe(format === 'text' ? 'truncated\n' : ''); + expect(stderr).not.toContain('[Duration:'); }, ); } finally {