@@ -1015,3 +1015,141 @@ async def test_partial_streaming_continuation(self, translator, mock_adk_event_w
10151015 # Should reset streaming state
10161016 assert translator ._is_streaming is False
10171017 assert translator ._streaming_message_id is None
1018+
1019+ @pytest .fixture
1020+ def mock_adk_event_empty_text (self ):
1021+ """Create a mock ADK event with empty text content."""
1022+ event = MagicMock (spec = ADKEvent )
1023+ event .id = "test_event_id"
1024+ event .author = "model"
1025+
1026+ # Mock content with empty text part
1027+ mock_content = MagicMock ()
1028+ mock_part = MagicMock ()
1029+ mock_part .text = ""
1030+ mock_content .parts = [mock_part ]
1031+ event .content = mock_content
1032+
1033+ event .partial = False
1034+ event .turn_complete = True
1035+ event .is_final_response = False
1036+ return event
1037+
1038+ @pytest .mark .asyncio
1039+ async def test_empty_text_event_does_not_crash (self , translator , mock_adk_event_empty_text ):
1040+ """Test that empty text events are filtered and don't crash the frontend.
1041+
1042+ Previously, empty text content would cause AG-UI's TextMessageContentEvent
1043+ validation to fail. The fix filters out empty text before reaching validation.
1044+ """
1045+ events = []
1046+ async for event in translator .translate (mock_adk_event_empty_text , "thread_1" , "run_1" ):
1047+ events .append (event )
1048+
1049+ # Empty text should be filtered out - no events emitted
1050+ assert len (events ) == 0
1051+ content_events = [e for e in events if isinstance (e , TextMessageContentEvent )]
1052+ assert len (content_events ) == 0
1053+
1054+ @pytest .mark .asyncio
1055+ async def test_whitespace_only_text_event_does_not_crash (self , translator ):
1056+ """Test that whitespace-only text events are also handled.
1057+
1058+ While the current fix checks for empty string, whitespace-only
1059+ content should also not cause issues.
1060+ """
1061+ event = MagicMock (spec = ADKEvent )
1062+ event .id = "test_event_id"
1063+ event .author = "model"
1064+
1065+ mock_content = MagicMock ()
1066+ mock_part = MagicMock ()
1067+ mock_part .text = " " # Whitespace only
1068+ mock_content .parts = [mock_part ]
1069+ event .content = mock_content
1070+
1071+ event .partial = False
1072+ event .turn_complete = True
1073+ event .is_final_response = False
1074+
1075+ events = []
1076+ async for event in translator .translate (event , "thread_1" , "run_1" ):
1077+ events .append (event )
1078+
1079+ # Whitespace is valid text content, should be emitted
1080+ content_events = [e for e in events if isinstance (e , TextMessageContentEvent )]
1081+ assert len (content_events ) == 1
1082+ assert content_events [0 ].delta == " "
1083+
1084+ @pytest .mark .asyncio
1085+ async def test_multiple_empty_parts_filtered (self , translator , mock_adk_event ):
1086+ """Test that multiple empty text parts are all filtered."""
1087+ mock_content = MagicMock ()
1088+ mock_part1 = MagicMock ()
1089+ mock_part1 .text = ""
1090+ mock_part2 = MagicMock ()
1091+ mock_part2 .text = ""
1092+ mock_part3 = MagicMock ()
1093+ mock_part3 .text = ""
1094+ mock_content .parts = [mock_part1 , mock_part2 , mock_part3 ]
1095+ mock_adk_event .content = mock_content
1096+
1097+ events = []
1098+ async for event in translator .translate (mock_adk_event , "thread_1" , "run_1" ):
1099+ events .append (event )
1100+
1101+ # All empty parts should result in no text events
1102+ assert len (events ) == 0
1103+
1104+ @pytest .mark .asyncio
1105+ async def test_mixed_empty_and_valid_parts_filtering (self , translator , mock_adk_event ):
1106+ """Test that valid text parts are still emitted when mixed with empty parts."""
1107+ mock_content = MagicMock ()
1108+ mock_part1 = MagicMock ()
1109+ mock_part1 .text = ""
1110+ mock_part2 = MagicMock ()
1111+ mock_part2 .text = "Valid content"
1112+ mock_part3 = MagicMock ()
1113+ mock_part3 .text = ""
1114+ mock_content .parts = [mock_part1 , mock_part2 , mock_part3 ]
1115+ mock_adk_event .content = mock_content
1116+
1117+ events = []
1118+ async for event in translator .translate (mock_adk_event , "thread_1" , "run_1" ):
1119+ events .append (event )
1120+
1121+ # Valid content should still be emitted
1122+ content_events = [e for e in events if isinstance (e , TextMessageContentEvent )]
1123+ assert len (content_events ) == 1
1124+ assert content_events [0 ].delta == "Valid content"
1125+
1126+ @pytest .mark .asyncio
1127+ async def test_empty_combined_text_early_return (self , translator , mock_adk_event ):
1128+ """Test the early return when combined_text is empty.
1129+
1130+ This directly tests the fix at lines 281-283:
1131+ if not combined_text:
1132+ return
1133+ """
1134+ # Create content where all parts have empty/None text
1135+ mock_content = MagicMock ()
1136+ mock_part1 = MagicMock ()
1137+ mock_part1 .text = ""
1138+ mock_part2 = MagicMock ()
1139+ mock_part2 .text = None
1140+ mock_content .parts = [mock_part1 , mock_part2 ]
1141+ mock_adk_event .content = mock_content
1142+
1143+ # Verify the translator doesn't start streaming for empty content
1144+ assert translator ._is_streaming is False
1145+
1146+ events = []
1147+ async for event in translator .translate (mock_adk_event , "thread_1" , "run_1" ):
1148+ events .append (event )
1149+
1150+ # No streaming should have started
1151+ assert translator ._is_streaming is False
1152+ assert len (events ) == 0
1153+ # No TextMessageStartEvent should be created for empty content
1154+ start_events = [e for e in events if isinstance (e , TextMessageStartEvent )]
1155+ assert len (start_events ) == 0
0 commit comments