@@ -174,6 +174,70 @@ describe("transport send events", () => {
174174 } ) ;
175175} ) ;
176176
177+ describe ( "stopped turn followed by a new turn" , ( ) => {
178+ /**
179+ * `.out` stub that honours the `Last-Event-ID` cursor like the server does, so
180+ * a resubscribe cannot replay records the reader already consumed. A stop that
181+ * never saw its turn-complete is therefore unrecoverable unless the new send
182+ * clears the skip state.
183+ */
184+ function cursoredOneTurnTransport ( ) {
185+ const frames = [
186+ { id : "1" , data : `{"type":"text-delta","id":"t1","delta":"hello"}` } ,
187+ { id : "2" , data : `{"type":"trigger:turn-complete"}` } ,
188+ ] ;
189+
190+ return makeTransport ( {
191+ fetch : async ( _url , init , ctx ) => {
192+ if ( ctx . endpoint === "in" ) return jsonOk ( ) ;
193+
194+ const cursor = new Headers ( init . headers ) . get ( "Last-Event-ID" ) ;
195+ const from = cursor ? frames . findIndex ( ( f ) => f . id === cursor ) + 1 : 0 ;
196+ const remaining = frames . slice ( from ) ;
197+ const response = sseResponse (
198+ remaining . map ( ( f ) => `id: ${ f . id } \ndata: ${ f . data } \n\n` ) . join ( "" )
199+ ) ;
200+ // Nothing left to send: the session is settled, so the reader stops
201+ // instead of resubscribing.
202+ if ( remaining . length === 0 ) response . headers . set ( "X-Session-Settled" , "true" ) ;
203+ return response ;
204+ } ,
205+ } ) ;
206+ }
207+
208+ it ( "streams a sendMessages turn after a stop that never saw turn-complete" , async ( ) => {
209+ const { transport, events } = cursoredOneTurnTransport ( ) ;
210+
211+ expect ( await transport . stopGeneration ( "c1" ) ) . toBe ( true ) ;
212+ events . length = 0 ;
213+
214+ const stream = await transport . sendMessages ( {
215+ trigger : "submit-message" ,
216+ chatId : "c1" ,
217+ messageId : undefined ,
218+ messages : [ user ( "after stop" , "u-2" ) ] ,
219+ abortSignal : undefined ,
220+ } ) ;
221+ const chunks = await readAll ( stream ) ;
222+
223+ expect ( chunks ) . toEqual ( [ { type : "text-delta" , id : "t1" , delta : "hello" } ] ) ;
224+ expect ( events . some ( ( e ) => e . type === "turn-completed" ) ) . toBe ( true ) ;
225+ } ) ;
226+
227+ it ( "streams a sendAction turn after a stop that never saw turn-complete" , async ( ) => {
228+ const { transport, events } = cursoredOneTurnTransport ( ) ;
229+
230+ expect ( await transport . stopGeneration ( "c1" ) ) . toBe ( true ) ;
231+ events . length = 0 ;
232+
233+ const stream = await transport . sendAction ( "c1" , { type : "undo" } ) ;
234+ const chunks = await readAll ( stream ) ;
235+
236+ expect ( chunks ) . toEqual ( [ { type : "text-delta" , id : "t1" , delta : "hello" } ] ) ;
237+ expect ( events . some ( ( e ) => e . type === "turn-completed" ) ) . toBe ( true ) ;
238+ } ) ;
239+ } ) ;
240+
177241describe ( "transport stream events" , ( ) => {
178242 it ( "marks reconnectToStream subscriptions as resumed" , async ( ) => {
179243 const { transport, events } = makeTransport ( {
0 commit comments