@@ -27,7 +27,7 @@ import {
2727 extractEditContent ,
2828 runStreamLoop ,
2929} from '@/lib/copilot/request/go/stream'
30- import { createEvent , hasAbortMarker } from '@/lib/copilot/request/session'
30+ import { AbortReason , createEvent , hasAbortMarker } from '@/lib/copilot/request/session'
3131import { RequestTraceV1Outcome , TraceCollector } from '@/lib/copilot/request/trace'
3232import type { ExecutionContext , StreamingContext } from '@/lib/copilot/request/types'
3333
@@ -331,6 +331,72 @@ describe('copilot go stream helpers', () => {
331331 ) . toBe ( false )
332332 } )
333333
334+ it ( 'invokes onAbortObserved with MarkerObservedAtBodyClose when reclassifying via the abort marker' , async ( ) => {
335+ const textEvent = createEvent ( {
336+ streamId : 'stream-1' ,
337+ cursor : '1' ,
338+ seq : 1 ,
339+ requestId : 'req-1' ,
340+ type : MothershipStreamV1EventType . text ,
341+ payload : {
342+ channel : 'assistant' ,
343+ text : 'partial response' ,
344+ } ,
345+ } )
346+
347+ vi . mocked ( fetch ) . mockResolvedValueOnce ( createSseResponse ( [ textEvent ] ) )
348+ vi . mocked ( hasAbortMarker ) . mockResolvedValueOnce ( true )
349+
350+ const context = createStreamingContext ( )
351+ const execContext : ExecutionContext = {
352+ userId : 'user-1' ,
353+ workflowId : 'workflow-1' ,
354+ }
355+ const onAbortObserved = vi . fn ( )
356+
357+ await runStreamLoop ( 'https://example.com/mothership/stream' , { } , context , execContext , {
358+ timeout : 1000 ,
359+ onAbortObserved,
360+ } )
361+
362+ expect ( onAbortObserved ) . toHaveBeenCalledTimes ( 1 )
363+ expect ( onAbortObserved ) . toHaveBeenCalledWith ( AbortReason . MarkerObservedAtBodyClose )
364+ expect ( context . wasAborted ) . toBe ( true )
365+ } )
366+
367+ it ( 'does not invoke onAbortObserved when no abort marker is present at body close' , async ( ) => {
368+ const textEvent = createEvent ( {
369+ streamId : 'stream-1' ,
370+ cursor : '1' ,
371+ seq : 1 ,
372+ requestId : 'req-1' ,
373+ type : MothershipStreamV1EventType . text ,
374+ payload : {
375+ channel : 'assistant' ,
376+ text : 'partial response' ,
377+ } ,
378+ } )
379+
380+ vi . mocked ( fetch ) . mockResolvedValueOnce ( createSseResponse ( [ textEvent ] ) )
381+ vi . mocked ( hasAbortMarker ) . mockResolvedValueOnce ( false )
382+
383+ const context = createStreamingContext ( )
384+ const execContext : ExecutionContext = {
385+ userId : 'user-1' ,
386+ workflowId : 'workflow-1' ,
387+ }
388+ const onAbortObserved = vi . fn ( )
389+
390+ await expect (
391+ runStreamLoop ( 'https://example.com/mothership/stream' , { } , context , execContext , {
392+ timeout : 1000 ,
393+ onAbortObserved,
394+ } )
395+ ) . rejects . toThrow ( 'Copilot backend stream ended before a terminal event' )
396+
397+ expect ( onAbortObserved ) . not . toHaveBeenCalled ( )
398+ } )
399+
334400 it ( 'still fails closed when the body closes without terminal and the abort marker check throws' , async ( ) => {
335401 const textEvent = createEvent ( {
336402 streamId : 'stream-1' ,
0 commit comments