@@ -10,13 +10,24 @@ import {
1010 MothershipStreamV1ToolOutcome ,
1111 MothershipStreamV1ToolPhase ,
1212} from '@/lib/copilot/generated/mothership-stream-v1'
13+
14+ vi . mock ( '@/lib/copilot/request/session' , async ( ) => {
15+ const actual = await vi . importActual < typeof import ( '@/lib/copilot/request/session' ) > (
16+ '@/lib/copilot/request/session'
17+ )
18+ return {
19+ ...actual ,
20+ hasAbortMarker : vi . fn ( ) . mockResolvedValue ( false ) ,
21+ }
22+ } )
23+
1324import {
1425 buildPreviewContentUpdate ,
1526 decodeJsonStringPrefix ,
1627 extractEditContent ,
1728 runStreamLoop ,
1829} from '@/lib/copilot/request/go/stream'
19- import { createEvent } from '@/lib/copilot/request/session'
30+ import { createEvent , hasAbortMarker } from '@/lib/copilot/request/session'
2031import { RequestTraceV1Outcome , TraceCollector } from '@/lib/copilot/request/trace'
2132import type { ExecutionContext , StreamingContext } from '@/lib/copilot/request/types'
2233
@@ -285,6 +296,71 @@ describe('copilot go stream helpers', () => {
285296 ) . toBe ( true )
286297 } )
287298
299+ it ( 'reclassifies as aborted when the body closes without terminal but the abort marker is set' , async ( ) => {
300+ const textEvent = createEvent ( {
301+ streamId : 'stream-1' ,
302+ cursor : '1' ,
303+ seq : 1 ,
304+ requestId : 'req-1' ,
305+ type : MothershipStreamV1EventType . text ,
306+ payload : {
307+ channel : 'assistant' ,
308+ text : 'partial response' ,
309+ } ,
310+ } )
311+
312+ vi . mocked ( fetch ) . mockResolvedValueOnce ( createSseResponse ( [ textEvent ] ) )
313+ vi . mocked ( hasAbortMarker ) . mockResolvedValueOnce ( true )
314+
315+ const context = createStreamingContext ( )
316+ const execContext : ExecutionContext = {
317+ userId : 'user-1' ,
318+ workflowId : 'workflow-1' ,
319+ }
320+
321+ await runStreamLoop ( 'https://example.com/mothership/stream' , { } , context , execContext , {
322+ timeout : 1000 ,
323+ } )
324+
325+ expect ( hasAbortMarker ) . toHaveBeenCalledWith ( context . messageId )
326+ expect ( context . wasAborted ) . toBe ( true )
327+ expect (
328+ context . errors . some ( ( message ) =>
329+ message . includes ( 'Copilot backend stream ended before a terminal event' )
330+ )
331+ ) . toBe ( false )
332+ } )
333+
334+ it ( 'still fails closed when the body closes without terminal and the abort marker check throws' , 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 ) . mockRejectedValueOnce ( new Error ( 'redis unavailable' ) )
349+
350+ const context = createStreamingContext ( )
351+ const execContext : ExecutionContext = {
352+ userId : 'user-1' ,
353+ workflowId : 'workflow-1' ,
354+ }
355+
356+ await expect (
357+ runStreamLoop ( 'https://example.com/mothership/stream' , { } , context , execContext , {
358+ timeout : 1000 ,
359+ } )
360+ ) . rejects . toThrow ( 'Copilot backend stream ended before a terminal event' )
361+ expect ( context . wasAborted ) . toBe ( false )
362+ } )
363+
288364 it ( 'fails closed when the shared stream receives an invalid event' , async ( ) => {
289365 vi . mocked ( fetch ) . mockResolvedValueOnce (
290366 createSseResponse ( [
0 commit comments