@@ -1136,6 +1136,86 @@ describe("TriggerChatTransport", () => {
11361136 onSessionChange . mock . calls . some ( ( [ , session ] ) => session && session . isStreaming === false )
11371137 ) . toBe ( true ) ;
11381138 } ) ;
1139+
1140+ it ( "keeps streaming when every window delivers a single record" , async ( ) => {
1141+ // One record per window arrives via `primed` on the resumed connection —
1142+ // it must still re-earn the budget, otherwise a slow turn is truncated.
1143+ const WINDOWS = 8 ;
1144+ let subscribeCount = 0 ;
1145+ global . fetch = vi . fn ( ) . mockImplementation ( async ( url : string | URL ) => {
1146+ const urlStr = typeof url === "string" ? url : url . toString ( ) ;
1147+ if ( isSessionStreamAppendUrl ( urlStr ) ) return defaultAppendResponse ( ) ;
1148+ if ( isSessionOutSubscribeUrl ( urlStr ) ) {
1149+ subscribeCount ++ ;
1150+ return subscribeCount > WINDOWS
1151+ ? defaultSseResponse ( [ { type : "trigger:turn-complete" } ] )
1152+ : defaultSseResponse ( [
1153+ { type : "text-delta" , id : "part-1" , delta : `d${ subscribeCount } ` } ,
1154+ ] ) ;
1155+ }
1156+ throw new Error ( `Unexpected URL: ${ urlStr } ` ) ;
1157+ } ) ;
1158+
1159+ const transport = new TriggerChatTransport ( {
1160+ task : "my-chat-task" ,
1161+ accessToken : ( ) => "pat" ,
1162+ sessions : { "chat-slow" : { publicAccessToken : "p" } } ,
1163+ } ) ;
1164+
1165+ const stream = await transport . sendMessages ( {
1166+ trigger : "submit-message" ,
1167+ chatId : "chat-slow" ,
1168+ messageId : undefined ,
1169+ messages : [ createUserMessage ( "hi" ) ] ,
1170+ abortSignal : undefined ,
1171+ } ) ;
1172+ const chunks = await drainChunks ( stream ) ;
1173+
1174+ expect ( subscribeCount ) . toBe ( WINDOWS + 1 ) ;
1175+ expect ( chunks ) . toHaveLength ( WINDOWS ) ;
1176+ expect ( transport . getSession ( "chat-slow" ) ?. isStreaming ) . toBe ( false ) ;
1177+ } ) ;
1178+
1179+ it ( "gives up after a bounded number of resubscribes" , async ( ) => {
1180+ // Fake timers so the 100ms..1.6s backoffs don't cost real seconds.
1181+ vi . useFakeTimers ( ) ;
1182+ try {
1183+ let subscribeCount = 0 ;
1184+ global . fetch = vi . fn ( ) . mockImplementation ( async ( url : string | URL ) => {
1185+ const urlStr = typeof url === "string" ? url : url . toString ( ) ;
1186+ if ( isSessionStreamAppendUrl ( urlStr ) ) return defaultAppendResponse ( ) ;
1187+ if ( isSessionOutSubscribeUrl ( urlStr ) ) {
1188+ subscribeCount ++ ;
1189+ // Never any records, never settled — the pathological case.
1190+ return defaultSseResponse ( [ ] ) ;
1191+ }
1192+ throw new Error ( `Unexpected URL: ${ urlStr } ` ) ;
1193+ } ) ;
1194+
1195+ const transport = new TriggerChatTransport ( {
1196+ task : "my-chat-task" ,
1197+ accessToken : ( ) => "pat" ,
1198+ sessions : { "chat-empty" : { publicAccessToken : "p" } } ,
1199+ } ) ;
1200+
1201+ const stream = await transport . sendMessages ( {
1202+ trigger : "submit-message" ,
1203+ chatId : "chat-empty" ,
1204+ messageId : undefined ,
1205+ messages : [ createUserMessage ( "hi" ) ] ,
1206+ abortSignal : undefined ,
1207+ } ) ;
1208+ const drained = drainChunks ( stream ) ;
1209+ await vi . advanceTimersByTimeAsync ( 10_000 ) ;
1210+ await drained ;
1211+
1212+ // One initial connect plus the five-attempt resubscribe budget.
1213+ expect ( subscribeCount ) . toBe ( 6 ) ;
1214+ expect ( transport . getSession ( "chat-empty" ) ?. isStreaming ) . toBe ( false ) ;
1215+ } finally {
1216+ vi . useRealTimers ( ) ;
1217+ }
1218+ } ) ;
11391219 } ) ;
11401220
11411221 describe ( "multi-tab coordination" , ( ) => {
0 commit comments