@@ -395,9 +395,39 @@ async fn stream_from_fixture(path: impl AsRef<Path>) -> Result<ResponseStream> {
395
395
#[ cfg( test) ]
396
396
mod tests {
397
397
#![ allow( clippy:: expect_used, clippy:: unwrap_used) ]
398
+
398
399
use super :: * ;
399
400
use serde_json:: json;
401
+ use tokio:: sync:: mpsc;
402
+ use tokio_test:: io:: Builder as IoBuilder ;
403
+ use tokio_util:: io:: ReaderStream ;
404
+
405
+ // ────────────────────────────
406
+ // Helpers
407
+ // ────────────────────────────
408
+
409
+ /// Runs the SSE parser on pre-chunked byte slices and returns every event
410
+ /// (including any final `Err` from a stream-closure check).
411
+ async fn collect_events ( chunks : & [ & [ u8 ] ] ) -> Vec < Result < ResponseEvent > > {
412
+ let mut builder = IoBuilder :: new ( ) ;
413
+ for chunk in chunks {
414
+ builder. read ( chunk) ;
415
+ }
416
+
417
+ let reader = builder. build ( ) ;
418
+ let stream = ReaderStream :: new ( reader) . map_err ( CodexErr :: Io ) ;
419
+ let ( tx, mut rx) = mpsc:: channel :: < Result < ResponseEvent > > ( 16 ) ;
420
+ tokio:: spawn ( process_sse ( stream, tx) ) ;
400
421
422
+ let mut events = Vec :: new ( ) ;
423
+ while let Some ( ev) = rx. recv ( ) . await {
424
+ events. push ( ev) ;
425
+ }
426
+ events
427
+ }
428
+
429
+ /// Builds an in-memory SSE stream from JSON fixtures and returns only the
430
+ /// successfully parsed events (panics on internal channel errors).
401
431
async fn run_sse ( events : Vec < serde_json:: Value > ) -> Vec < ResponseEvent > {
402
432
let mut body = String :: new ( ) ;
403
433
for e in events {
@@ -411,24 +441,116 @@ mod tests {
411
441
body. push_str ( & format ! ( "event: {kind}\n data: {e}\n \n " ) ) ;
412
442
}
413
443
}
444
+
414
445
let ( tx, mut rx) = mpsc:: channel :: < Result < ResponseEvent > > ( 8 ) ;
415
446
let stream = ReaderStream :: new ( std:: io:: Cursor :: new ( body) ) . map_err ( CodexErr :: Io ) ;
416
447
tokio:: spawn ( process_sse ( stream, tx) ) ;
448
+
417
449
let mut out = Vec :: new ( ) ;
418
450
while let Some ( ev) = rx. recv ( ) . await {
419
451
out. push ( ev. expect ( "channel closed" ) ) ;
420
452
}
421
453
out
422
454
}
423
455
424
- /// Verifies that the SSE adapter emits the expected [`ResponseEvent`] for
425
- /// a variety of `type` values from the Responses API. The test is written
426
- /// table-driven style to keep additions for new event kinds trivial.
427
- ///
428
- /// Each `Case` supplies an input event, a predicate that must match the
429
- /// *first* `ResponseEvent` produced by the adapter, and the total number
430
- /// of events expected after appending a synthetic `response.completed`
431
- /// marker that terminates the stream.
456
+ // ────────────────────────────
457
+ // Tests from `implement-test-for-responses-api-sse-parser`
458
+ // ────────────────────────────
459
+
460
+ #[ tokio:: test]
461
+ async fn parses_items_and_completed ( ) {
462
+ let item1 = json ! ( {
463
+ "type" : "response.output_item.done" ,
464
+ "item" : {
465
+ "type" : "message" ,
466
+ "role" : "assistant" ,
467
+ "content" : [ { "type" : "output_text" , "text" : "Hello" } ]
468
+ }
469
+ } )
470
+ . to_string ( ) ;
471
+
472
+ let item2 = json ! ( {
473
+ "type" : "response.output_item.done" ,
474
+ "item" : {
475
+ "type" : "message" ,
476
+ "role" : "assistant" ,
477
+ "content" : [ { "type" : "output_text" , "text" : "World" } ]
478
+ }
479
+ } )
480
+ . to_string ( ) ;
481
+
482
+ let completed = json ! ( {
483
+ "type" : "response.completed" ,
484
+ "response" : { "id" : "resp1" }
485
+ } )
486
+ . to_string ( ) ;
487
+
488
+ let sse1 = format ! ( "event: response.output_item.done\n data: {item1}\n \n " ) ;
489
+ let sse2 = format ! ( "event: response.output_item.done\n data: {item2}\n \n " ) ;
490
+ let sse3 = format ! ( "event: response.completed\n data: {completed}\n \n " ) ;
491
+
492
+ let events = collect_events ( & [ sse1. as_bytes ( ) , sse2. as_bytes ( ) , sse3. as_bytes ( ) ] ) . await ;
493
+
494
+ assert_eq ! ( events. len( ) , 3 ) ;
495
+
496
+ matches ! (
497
+ & events[ 0 ] ,
498
+ Ok ( ResponseEvent :: OutputItemDone ( ResponseItem :: Message { role, .. } ) )
499
+ if role == "assistant"
500
+ ) ;
501
+
502
+ matches ! (
503
+ & events[ 1 ] ,
504
+ Ok ( ResponseEvent :: OutputItemDone ( ResponseItem :: Message { role, .. } ) )
505
+ if role == "assistant"
506
+ ) ;
507
+
508
+ match & events[ 2 ] {
509
+ Ok ( ResponseEvent :: Completed {
510
+ response_id,
511
+ token_usage,
512
+ } ) => {
513
+ assert_eq ! ( response_id, "resp1" ) ;
514
+ assert ! ( token_usage. is_none( ) ) ;
515
+ }
516
+ other => panic ! ( "unexpected third event: {other:?}" ) ,
517
+ }
518
+ }
519
+
520
+ #[ tokio:: test]
521
+ async fn error_when_missing_completed ( ) {
522
+ let item1 = json ! ( {
523
+ "type" : "response.output_item.done" ,
524
+ "item" : {
525
+ "type" : "message" ,
526
+ "role" : "assistant" ,
527
+ "content" : [ { "type" : "output_text" , "text" : "Hello" } ]
528
+ }
529
+ } )
530
+ . to_string ( ) ;
531
+
532
+ let sse1 = format ! ( "event: response.output_item.done\n data: {item1}\n \n " ) ;
533
+
534
+ let events = collect_events ( & [ sse1. as_bytes ( ) ] ) . await ;
535
+
536
+ assert_eq ! ( events. len( ) , 2 ) ;
537
+
538
+ matches ! ( events[ 0 ] , Ok ( ResponseEvent :: OutputItemDone ( _) ) ) ;
539
+
540
+ match & events[ 1 ] {
541
+ Err ( CodexErr :: Stream ( msg) ) => {
542
+ assert_eq ! ( msg, "stream closed before response.completed" )
543
+ }
544
+ other => panic ! ( "unexpected second event: {other:?}" ) ,
545
+ }
546
+ }
547
+
548
+ // ────────────────────────────
549
+ // Table-driven test from `main`
550
+ // ────────────────────────────
551
+
552
+ /// Verifies that the adapter produces the right `ResponseEvent` for a
553
+ /// variety of incoming `type` values.
432
554
#[ tokio:: test]
433
555
async fn table_driven_event_kinds ( ) {
434
556
struct TestCase {
@@ -441,11 +563,9 @@ mod tests {
441
563
fn is_created ( ev : & ResponseEvent ) -> bool {
442
564
matches ! ( ev, ResponseEvent :: Created )
443
565
}
444
-
445
566
fn is_output ( ev : & ResponseEvent ) -> bool {
446
567
matches ! ( ev, ResponseEvent :: OutputItemDone ( _) )
447
568
}
448
-
449
569
fn is_completed ( ev : & ResponseEvent ) -> bool {
450
570
matches ! ( ev, ResponseEvent :: Completed { .. } )
451
571
}
@@ -498,9 +618,14 @@ mod tests {
498
618
for case in cases {
499
619
let mut evs = vec ! [ case. event] ;
500
620
evs. push ( completed. clone ( ) ) ;
621
+
501
622
let out = run_sse ( evs) . await ;
502
623
assert_eq ! ( out. len( ) , case. expected_len, "case {}" , case. name) ;
503
- assert ! ( ( case. expect_first) ( & out[ 0 ] ) , "case {}" , case. name) ;
624
+ assert ! (
625
+ ( case. expect_first) ( & out[ 0 ] ) ,
626
+ "first event mismatch in case {}" ,
627
+ case. name
628
+ ) ;
504
629
}
505
630
}
506
631
}
0 commit comments