@@ -496,6 +496,72 @@ describe("TanStack AI converter — reasoning", () => {
496496 expect ( types ) . toContain ( EventType . REASONING_END ) ;
497497 } ) ;
498498
499+ it ( "emits REASONING_MESSAGE_END before REASONING_END when upstream sends END with message still open" , async ( ) => {
500+ // Regression: if the converter received REASONING_END while a message
501+ // was still open, it cleared run-open and emitted END only — leaving
502+ // message-open true. The next non-reasoning chunk then triggered
503+ // closeReasoningIfOpen, which emitted MSG_END AFTER END (wrong order).
504+ // Fix: REASONING_END handler closes any open message first.
505+ const agent = createAgent ( "tanstack" , [
506+ tanstackReasoningStart ( "r1" ) ,
507+ tanstackReasoningMessageStart ( "r1" ) ,
508+ tanstackReasoningMessageContent ( "r1" , "thinking" ) ,
509+ // Upstream skips MSG_END and goes straight to END
510+ tanstackReasoningEnd ( "r1" ) ,
511+ tanstackTextChunk ( "Hi" ) ,
512+ ] ) ;
513+ const events = await collectEvents ( agent . run ( createDefaultInput ( ) ) ) ;
514+ const types = events . map ( ( e ) => e . type ) ;
515+
516+ const msgEndIdx = types . indexOf ( EventType . REASONING_MESSAGE_END ) ;
517+ const endIdx = types . indexOf ( EventType . REASONING_END ) ;
518+
519+ expect ( msgEndIdx ) . toBeGreaterThan ( - 1 ) ;
520+ expect ( endIdx ) . toBeGreaterThan ( - 1 ) ;
521+ expect ( msgEndIdx ) . toBeLessThan ( endIdx ) ;
522+ // No duplicate MSG_END or END from auto-close on the text chunk
523+ expect (
524+ types . filter ( ( t ) => t === EventType . REASONING_MESSAGE_END ) ,
525+ ) . toHaveLength ( 1 ) ;
526+ expect ( types . filter ( ( t ) => t === EventType . REASONING_END ) ) . toHaveLength ( 1 ) ;
527+ } ) ;
528+
529+ it ( "auto-closes prior reasoning run when a new REASONING_START arrives without END" , async ( ) => {
530+ // Regression: REASONING_START used to overwrite reasoningMessageId
531+ // unconditionally, orphaning the prior run's MSG_END / END.
532+ // Fix: REASONING_START handler calls closeReasoningIfOpen() first.
533+ const agent = createAgent ( "tanstack" , [
534+ tanstackReasoningStart ( "r1" ) ,
535+ tanstackReasoningMessageStart ( "r1" ) ,
536+ tanstackReasoningMessageContent ( "r1" , "first" ) ,
537+ // Second START with no intervening END
538+ tanstackReasoningStart ( "r2" ) ,
539+ tanstackReasoningMessageStart ( "r2" ) ,
540+ tanstackReasoningMessageContent ( "r2" , "second" ) ,
541+ tanstackReasoningMessageEnd ( "r2" ) ,
542+ tanstackReasoningEnd ( "r2" ) ,
543+ ] ) ;
544+ const events = await collectEvents ( agent . run ( createDefaultInput ( ) ) ) ;
545+ const types = events . map ( ( e ) => e . type ) ;
546+
547+ // Two complete START → MSG_START → ... → MSG_END → END sequences
548+ expect (
549+ types . filter ( ( t ) => t === EventType . REASONING_START ) ,
550+ ) . toHaveLength ( 2 ) ;
551+ expect ( types . filter ( ( t ) => t === EventType . REASONING_END ) ) . toHaveLength ( 2 ) ;
552+ expect (
553+ types . filter ( ( t ) => t === EventType . REASONING_MESSAGE_END ) ,
554+ ) . toHaveLength ( 2 ) ;
555+
556+ // First START's prior message gets closed BEFORE the second START
557+ const firstEndIdx = types . indexOf ( EventType . REASONING_END ) ;
558+ const secondStartIdx = types . indexOf (
559+ EventType . REASONING_START ,
560+ firstEndIdx + 1 ,
561+ ) ;
562+ expect ( firstEndIdx ) . toBeLessThan ( secondStartIdx ) ;
563+ } ) ;
564+
499565 it ( "does NOT duplicate REASONING_MESSAGE_END when upstream emits it explicitly before text" , async ( ) => {
500566 // Regression: a single isInReasoning flag conflated message-open with
501567 // run-open, so closeReasoningIfOpen on TEXT_MESSAGE_CONTENT emitted a
0 commit comments