@@ -12,11 +12,18 @@ async function collect(gen: AsyncGenerator<AdapterEvent>): Promise<AdapterEvent[
1212}
1313
1414describe ( "openai-chat stream EOF fail-closed" , ( ) => {
15- test ( "truncated stream (no [DONE], no finish_reason) yields a terminal error, not a clean done " , async ( ) => {
15+ test ( "truncated stream (no [DONE], no finish_reason) yields done when content was emitted " , async ( ) => {
1616 const response = new Response ( 'data: {"choices":[{"delta":{"content":"par"}}]}\n\n' ) ;
1717 const events = await collect ( createOpenAIChatAdapter ( provider ) . parseStream ( response ) ) ;
1818 const last = events [ events . length - 1 ] ;
19- expect ( last . type ) . toBe ( "error" ) ;
19+ expect ( last . type ) . toBe ( "done" ) ;
20+ expect ( events . some ( e => e . type === "error" ) ) . toBe ( false ) ;
21+ } ) ;
22+
23+ test ( "empty EOF without content still errors" , async ( ) => {
24+ const response = new Response ( "" ) ;
25+ const events = await collect ( createOpenAIChatAdapter ( provider ) . parseStream ( response ) ) ;
26+ expect ( events . at ( - 1 ) ?. type ) . toBe ( "error" ) ;
2027 expect ( events . some ( e => e . type === "done" ) ) . toBe ( false ) ;
2128 } ) ;
2229
@@ -128,10 +135,49 @@ describe("openai-chat stream EOF fail-closed", () => {
128135 expect ( events . some ( e => e . type === "error" ) ) . toBe ( false ) ;
129136 } ) ;
130137
131- test ( "genuinely truncated stream WITHOUT a trailing newline still fails closed " , async ( ) => {
132- // Mid-content frame, no terminator, no newline — must remain a terminal error .
138+ test ( "genuinely truncated stream WITHOUT a trailing newline completes when content was emitted " , async ( ) => {
139+ // Mid-content frame, no terminator, no newline — content was yielded, so accept done .
133140 const response = new Response ( 'data: {"choices":[{"delta":{"content":"par"}}]}' ) ;
134141 const events = await collect ( createOpenAIChatAdapter ( provider ) . parseStream ( response ) ) ;
142+ expect ( events . at ( - 1 ) ?. type ) . toBe ( "done" ) ;
143+ expect ( events . some ( e => e . type === "error" ) ) . toBe ( false ) ;
144+ } ) ;
145+
146+ test ( "EOF with pending tool calls and no finish_reason fails closed" , async ( ) => {
147+ const response = new Response (
148+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"get_weather","arguments":"{\\"a\\":"}}]}}]}\n\n' ,
149+ ) ;
150+ const events = await collect ( createOpenAIChatAdapter ( provider ) . parseStream ( response ) ) ;
151+ expect ( events . at ( - 1 ) ?. type ) . toBe ( "error" ) ;
152+ expect ( events . some ( e => e . type === "done" ) ) . toBe ( false ) ;
153+ expect ( events . some ( e => e . type === "tool_call_end" ) ) . toBe ( false ) ;
154+ } ) ;
155+
156+ test ( "reasoning-only EOF without finish_reason fails closed" , async ( ) => {
157+ const response = new Response (
158+ 'data: {"choices":[{"delta":{"reasoning_content":"thinking..."}}]}\n\n' ,
159+ ) ;
160+ const events = await collect ( createOpenAIChatAdapter ( provider ) . parseStream ( response ) ) ;
161+ expect ( events . at ( - 1 ) ?. type ) . toBe ( "error" ) ;
162+ expect ( events . some ( e => e . type === "done" ) ) . toBe ( false ) ;
163+ } ) ;
164+
165+ test ( "usage-only EOF with pending tool calls fails closed" , async ( ) => {
166+ const response = new Response (
167+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"get_weather","arguments":"{}"}}]}}]}\n\n' +
168+ 'data: {"choices":[],"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}\n\n' ,
169+ ) ;
170+ const events = await collect ( createOpenAIChatAdapter ( provider ) . parseStream ( response ) ) ;
171+ expect ( events . at ( - 1 ) ?. type ) . toBe ( "error" ) ;
172+ expect ( events . some ( e => e . type === "done" ) ) . toBe ( false ) ;
173+ expect ( events . some ( e => e . type === "tool_call_end" ) ) . toBe ( false ) ;
174+ } ) ;
175+
176+ test ( "usage-only EOF without answer text fails closed" , async ( ) => {
177+ const response = new Response (
178+ 'data: {"choices":[],"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}\n\n' ,
179+ ) ;
180+ const events = await collect ( createOpenAIChatAdapter ( provider ) . parseStream ( response ) ) ;
135181 expect ( events . at ( - 1 ) ?. type ) . toBe ( "error" ) ;
136182 expect ( events . some ( e => e . type === "done" ) ) . toBe ( false ) ;
137183 } ) ;
0 commit comments