@@ -328,6 +328,182 @@ mod tests {
328328 assert ! ( json[ "data" ] [ "update" ] . get( "rawInput" ) . is_none( ) ) ;
329329 }
330330
331+ #[ test]
332+ fn codex_image_tool_update_omits_base64_result ( ) {
333+ let large_png_base64 = format ! ( "iVBORw0KGgo{}" , "A" . repeat( 128 * 1024 ) ) ;
334+ let notif = SessionNotification :: new (
335+ "sess-1" ,
336+ SessionUpdate :: ToolCallUpdate ( SdkToolCallUpdate :: new (
337+ "ig_test_image" ,
338+ ToolCallUpdateFields :: new ( )
339+ . status ( SdkToolCallStatus :: InProgress )
340+ . raw_output ( json ! ( {
341+ "call_id" : "ig_test_image" ,
342+ "status" : "generating" ,
343+ "saved_path" : "/Users/test/.codex/generated_images/session/ig_test_image.png" ,
344+ "revised_prompt" : "一只小猫" ,
345+ "result" : large_png_base64
346+ } ) ) ,
347+ ) ) ,
348+ ) ;
349+
350+ let events = session_notification_to_events ( & notif) ;
351+ assert_eq ! ( events. len( ) , 1 ) ;
352+ let json = serde_json:: to_value ( & events[ 0 ] ) . unwrap ( ) ;
353+ let raw_output = & json[ "data" ] [ "update" ] [ "rawOutput" ] ;
354+
355+ assert_eq ! (
356+ raw_output[ "saved_path" ] ,
357+ "/Users/test/.codex/generated_images/session/ig_test_image.png"
358+ ) ;
359+ assert_eq ! (
360+ raw_output[ "image" ] [ "path" ] ,
361+ "/Users/test/.codex/generated_images/session/ig_test_image.png"
362+ ) ;
363+ assert_eq ! ( raw_output[ "result_omitted" ] , true ) ;
364+ assert ! ( raw_output. get( "result" ) . is_none( ) ) ;
365+ }
366+
367+ #[ test]
368+ fn codex_image_tool_update_with_saved_path_is_completed ( ) {
369+ let notif = SessionNotification :: new (
370+ "sess-1" ,
371+ SessionUpdate :: ToolCallUpdate ( SdkToolCallUpdate :: new (
372+ "ig_done_image" ,
373+ ToolCallUpdateFields :: new ( )
374+ . status ( SdkToolCallStatus :: InProgress )
375+ . raw_output ( json ! ( {
376+ "call_id" : "ig_done_image" ,
377+ "status" : "generating" ,
378+ "saved_path" : "/Users/test/.codex/generated_images/session/ig_done_image.png" ,
379+ "result" : format!( "iVBORw0KGgo{}" , "A" . repeat( 128 * 1024 ) )
380+ } ) ) ,
381+ ) ) ,
382+ ) ;
383+
384+ let events = session_notification_to_events ( & notif) ;
385+ assert_eq ! ( events. len( ) , 1 ) ;
386+ let json = serde_json:: to_value ( & events[ 0 ] ) . unwrap ( ) ;
387+
388+ assert_eq ! ( json[ "data" ] [ "update" ] [ "status" ] , "completed" ) ;
389+ assert_eq ! ( json[ "data" ] [ "update" ] [ "rawOutput" ] [ "status" ] , "completed" ) ;
390+ }
391+
392+ #[ test]
393+ fn codex_image_tool_update_keeps_small_text_result_in_progress ( ) {
394+ let notif = SessionNotification :: new (
395+ "sess-1" ,
396+ SessionUpdate :: ToolCallUpdate ( SdkToolCallUpdate :: new (
397+ "small-result" ,
398+ ToolCallUpdateFields :: new ( )
399+ . status ( SdkToolCallStatus :: InProgress )
400+ . raw_output ( json ! ( {
401+ "status" : "generating" ,
402+ "saved_path" : "/tmp/result.txt" ,
403+ "result" : "not an inline image"
404+ } ) ) ,
405+ ) ) ,
406+ ) ;
407+
408+ let events = session_notification_to_events ( & notif) ;
409+ let json = serde_json:: to_value ( & events[ 0 ] ) . unwrap ( ) ;
410+ let raw_output = & json[ "data" ] [ "update" ] [ "rawOutput" ] ;
411+
412+ assert_eq ! ( json[ "data" ] [ "update" ] [ "status" ] , "in_progress" ) ;
413+ assert_eq ! ( raw_output[ "result" ] , "not an inline image" ) ;
414+ assert ! ( raw_output. get( "image" ) . is_none( ) ) ;
415+ assert ! ( raw_output. get( "result_omitted" ) . is_none( ) ) ;
416+ }
417+
418+ #[ test]
419+ fn codex_image_tool_update_detects_image_mime_types_from_saved_path ( ) {
420+ let cases = [
421+ ( "photo.jpg" , "image/jpeg" , "/9j/" ) ,
422+ ( "photo.webp" , "image/webp" , "UklGR" ) ,
423+ ( "photo.gif" , "image/gif" , "data:image/gif;base64," ) ,
424+ ( "photo.bin" , "image/png" , "iVBORw0KGgo" ) ,
425+ ] ;
426+
427+ for ( file_name, expected_mime, prefix) in cases {
428+ let saved_path = format ! ( "/Users/test/.codex/generated_images/session/{file_name}" ) ;
429+ let notif = SessionNotification :: new (
430+ "sess-1" ,
431+ SessionUpdate :: ToolCallUpdate ( SdkToolCallUpdate :: new (
432+ "ig_mime" ,
433+ ToolCallUpdateFields :: new ( ) . raw_output ( json ! ( {
434+ "saved_path" : saved_path,
435+ "result" : format!( "{prefix}{}" , "A" . repeat( 128 * 1024 ) )
436+ } ) ) ,
437+ ) ) ,
438+ ) ;
439+
440+ let events = session_notification_to_events ( & notif) ;
441+ let json = serde_json:: to_value ( & events[ 0 ] ) . unwrap ( ) ;
442+
443+ assert_eq ! ( json[ "data" ] [ "update" ] [ "rawOutput" ] [ "image" ] [ "mime_type" ] , expected_mime) ;
444+ assert ! ( json[ "data" ] [ "update" ] [ "rawOutput" ] . get( "result" ) . is_none( ) ) ;
445+ }
446+ }
447+
448+ #[ test]
449+ fn codex_image_tool_update_omits_base64_without_saved_path ( ) {
450+ let large_png_base64 = format ! ( "iVBORw0KGgo{}" , "A" . repeat( 128 * 1024 ) ) ;
451+ let notif = SessionNotification :: new (
452+ "sess-1" ,
453+ SessionUpdate :: ToolCallUpdate ( SdkToolCallUpdate :: new (
454+ "ig_no_path" ,
455+ ToolCallUpdateFields :: new ( )
456+ . status ( SdkToolCallStatus :: InProgress )
457+ . raw_output ( json ! ( {
458+ "call_id" : "ig_no_path" ,
459+ "status" : "generating" ,
460+ "result" : large_png_base64
461+ } ) ) ,
462+ ) ) ,
463+ ) ;
464+
465+ let events = session_notification_to_events ( & notif) ;
466+ assert_eq ! ( events. len( ) , 1 ) ;
467+ let json = serde_json:: to_value ( & events[ 0 ] ) . unwrap ( ) ;
468+ let raw_output = & json[ "data" ] [ "update" ] [ "rawOutput" ] ;
469+
470+ // Oversized base64 must be stripped even though Codex did not save the file.
471+ assert ! ( raw_output. get( "result" ) . is_none( ) ) ;
472+ assert_eq ! ( raw_output[ "result_omitted" ] , true ) ;
473+ // No saved_path means we cannot offer a path-based preview, so no image object.
474+ assert ! ( raw_output. get( "image" ) . is_none( ) ) ;
475+ // Without a saved image the status must pass through unchanged.
476+ assert_eq ! ( json[ "data" ] [ "update" ] [ "status" ] , "in_progress" ) ;
477+ }
478+
479+ #[ test]
480+ fn codex_image_tool_update_preserves_failed_status ( ) {
481+ let notif = SessionNotification :: new (
482+ "sess-1" ,
483+ SessionUpdate :: ToolCallUpdate ( SdkToolCallUpdate :: new (
484+ "ig_failed_image" ,
485+ ToolCallUpdateFields :: new ( )
486+ . status ( SdkToolCallStatus :: Failed )
487+ . raw_output ( json ! ( {
488+ "call_id" : "ig_failed_image" ,
489+ "status" : "failed" ,
490+ "saved_path" : "/Users/test/.codex/generated_images/session/ig_failed_image.png" ,
491+ "result" : format!( "iVBORw0KGgo{}" , "A" . repeat( 128 * 1024 ) )
492+ } ) ) ,
493+ ) ) ,
494+ ) ;
495+
496+ let events = session_notification_to_events ( & notif) ;
497+ assert_eq ! ( events. len( ) , 1 ) ;
498+ let json = serde_json:: to_value ( & events[ 0 ] ) . unwrap ( ) ;
499+
500+ // A terminal `failed` status must never be rewritten to `completed`.
501+ assert_eq ! ( json[ "data" ] [ "update" ] [ "status" ] , "failed" ) ;
502+ assert_eq ! ( json[ "data" ] [ "update" ] [ "rawOutput" ] [ "status" ] , "failed" ) ;
503+ // The base64 payload is still stripped regardless of the failure.
504+ assert ! ( json[ "data" ] [ "update" ] [ "rawOutput" ] . get( "result" ) . is_none( ) ) ;
505+ }
506+
331507 #[ test]
332508 fn permission_request_maps_to_snake_case_event_data ( ) {
333509 let request = RequestPermissionRequest :: new (
0 commit comments