@@ -359,14 +359,30 @@ describe("CodexWebSocketTransport", () => {
359359 }
360360 const stream = transport . stream ( body ( ) , { ...options ( ) , signal : controller . signal } )
361361 expect ( ( await stream . next ( ) ) . value ) . toMatchObject ( { delta : "Partial" } )
362- controller . abort ( )
363- await expect ( stream . next ( ) ) . rejects . toThrow ( )
362+ const reason = new Error ( "Stopped" )
363+ controller . abort ( reason )
364+ await expect ( stream . next ( ) ) . rejects . toBe ( reason )
364365 reply = ( _request , socket ) => complete ( socket )
365366 await collectStream ( transport . stream ( body ( ) , options ( ) ) )
366367 expect ( connections ) . toBe ( 2 )
367368 expect ( requests [ 1 ] . previous_response_id ) . toBeUndefined ( )
368369 } )
369370
371+ it ( "preserves Stop when cancellation races with receiving completion" , async ( ) => {
372+ const controller = new AbortController ( )
373+ const reason = new Error ( "Stopped" )
374+ reply = ( _request , socket ) => {
375+ const scope = transport [ "requestScope" ]
376+ if ( ! scope ) throw new Error ( "Missing active request scope" )
377+ // The iterator already received this frame, but the consumer has not resumed yet.
378+ scope . requestManager . socket . once ( "message" , ( ) => controller . abort ( reason ) )
379+ complete ( socket )
380+ }
381+ await expect ( collectStream ( transport . stream ( body ( ) , { ...options ( ) , signal : controller . signal } ) ) ) . rejects . toBe (
382+ reason ,
383+ )
384+ } )
385+
370386 it ( "disposes an active stream and reconnects with full context on the next request" , async ( ) => {
371387 reply = ( _request , socket ) =>
372388 socket . send ( JSON . stringify ( { type : "response.output_text.delta" , delta : "Partial" } ) )
@@ -392,17 +408,20 @@ describe("CodexWebSocketTransport", () => {
392408 expect ( requests [ 1 ] . previous_response_id ) . toBeUndefined ( )
393409 } )
394410
395- it ( "disposes request deadlines while the consumer is paused on a partial response" , async ( ) => {
411+ it ( "disposes request deadlines while waiting for more response events " , async ( ) => {
396412 vi . useFakeTimers ( { toFake : [ "setTimeout" , "clearTimeout" ] } )
397413 reply = ( _request , socket ) =>
398414 socket . send ( JSON . stringify ( { type : "response.output_text.delta" , delta : "Partial" } ) )
399415 const stream = transport . stream ( body ( ) , options ( ) )
400416 try {
401417 await stream . next ( )
418+ expect ( vi . getTimerCount ( ) ) . toBe ( 0 )
419+ const next = expect ( stream . next ( ) ) . rejects . toThrow ( "closed before response completed" )
420+ await vi . advanceTimersByTimeAsync ( 0 )
402421 expect ( vi . getTimerCount ( ) ) . toBe ( 1 )
403422 await transport . dispose ( )
404423 expect ( vi . getTimerCount ( ) ) . toBe ( 0 )
405- await expect ( stream . next ( ) ) . rejects . toThrow ( "closed before response completed" )
424+ await next
406425 } finally {
407426 await stream . return ( undefined )
408427 vi . useRealTimers ( )
@@ -411,7 +430,9 @@ describe("CodexWebSocketTransport", () => {
411430
412431 it ( "bounds a silent response with a timeout" , async ( ) => {
413432 reply = ( ) => { }
414- await expect ( collectStream ( transport . stream ( body ( ) , { ...options ( ) , timeoutMs : 100 } ) ) ) . rejects . toThrow ( )
433+ await expect ( collectStream ( transport . stream ( body ( ) , { ...options ( ) , timeoutMs : 100 } ) ) ) . rejects . toThrow (
434+ "Codex WebSocket stream timed out" ,
435+ )
415436 } )
416437
417438 it . each ( [ "null" , "[]" , "{}" , '{"type":42}' , "invalid JSON" ] ) (
@@ -459,6 +480,40 @@ describe("CodexWebSocketTransport", () => {
459480 }
460481 } )
461482
483+ it . each ( [ false , true ] ) ( "preserves queued completion during a consumer pause (server closes: %s)" , async ( close ) => {
484+ vi . useFakeTimers ( { toFake : [ "setTimeout" , "clearTimeout" ] } )
485+ reply = ( _request , socket ) =>
486+ socket . send ( JSON . stringify ( { type : "response.output_text.delta" , delta : "Partial" } ) )
487+ const stream = transport . stream ( body ( ) , { ...options ( ) , timeoutMs : 100 } )
488+ try {
489+ expect ( ( await stream . next ( ) ) . value ) . toMatchObject ( { delta : "Partial" } )
490+ const scope = transport [ "requestScope" ]
491+ if ( ! scope ) throw new Error ( "Missing active request scope" )
492+ const received = once ( scope . requestManager . socket , "message" )
493+ const serverSocket = [ ...server . clients ] [ 0 ]
494+ complete ( serverSocket )
495+ await received
496+ if ( close ) {
497+ const closed = once ( scope . requestManager . socket , "close" )
498+ serverSocket . close ( )
499+ await closed
500+ }
501+ await vi . advanceTimersByTimeAsync ( 1_000 )
502+ expect ( ( await stream . next ( ) ) . value ) . toMatchObject ( {
503+ type : "response.completed" ,
504+ response : { id : "resp_1" , usage : { input_tokens : 1 , output_tokens : 2 } } ,
505+ } )
506+ expect ( ( await stream . next ( ) ) . done ) . toBe ( true )
507+ reply = ( _request , socket ) => complete ( socket )
508+ await collectStream ( transport . stream ( body ( ) , options ( ) ) )
509+ expect ( connections ) . toBe ( close ? 2 : 1 )
510+ expect ( requests [ 1 ] . previous_response_id ) . toBe ( close ? undefined : "resp_1" )
511+ } finally {
512+ await stream . return ( undefined )
513+ vi . useRealTimers ( )
514+ }
515+ } )
516+
462517 it ( "rejects overlapping requests without disturbing the active stream" , async ( ) => {
463518 reply = ( _request , socket ) =>
464519 socket . send ( JSON . stringify ( { type : "response.output_text.delta" , delta : "Partial" } ) )
0 commit comments