diff --git a/src/crewai/utilities/events/event_listener.py b/src/crewai/utilities/events/event_listener.py index 1917eca1f..bb57bb03c 100644 --- a/src/crewai/utilities/events/event_listener.py +++ b/src/crewai/utilities/events/event_listener.py @@ -120,6 +120,7 @@ class EventListener(BaseEventListener): "completed", final_string_output, ) + self.formatter.stop_live() @crewai_event_bus.on(CrewKickoffFailedEvent) def on_crew_failed(source, event: CrewKickoffFailedEvent): @@ -262,6 +263,7 @@ class EventListener(BaseEventListener): self.formatter.update_flow_status( self.formatter.current_flow_tree, event.flow_name, source.flow_id ) + self.formatter.stop_live() @crewai_event_bus.on(MethodExecutionStartedEvent) def on_method_execution_started(source, event: MethodExecutionStartedEvent): diff --git a/src/crewai/utilities/events/utils/console_formatter.py b/src/crewai/utilities/events/utils/console_formatter.py index 24f92e386..9a7e8e356 100644 --- a/src/crewai/utilities/events/utils/console_formatter.py +++ b/src/crewai/utilities/events/utils/console_formatter.py @@ -1753,3 +1753,9 @@ class ConsoleFormatter: Attempts=f"{retry_count + 1}", ) self.print_panel(content, "🛡️ Guardrail Failed", "red") + + def stop_live(self) -> None: + """Stop and clear any active Live session to restore normal terminal output.""" + if self._live: + self._live.stop() + self._live = None diff --git a/tests/test_rich_live_cleanup.py b/tests/test_rich_live_cleanup.py new file mode 100644 index 000000000..04d094870 --- /dev/null +++ b/tests/test_rich_live_cleanup.py @@ -0,0 +1,57 @@ +import logging +from io import StringIO +from unittest.mock import MagicMock, patch +from rich.logging import RichHandler +from rich.tree import Tree +from crewai.utilities.events.utils.console_formatter import ConsoleFormatter +from crewai.utilities.events.event_listener import EventListener + + +class TestRichLiveCleanup: + """Test that Rich Live sessions are properly cleaned up after CrewAI operations.""" + + def test_logging_works_after_tree_rendering(self): + """Test that logging output appears after tree rendering with proper cleanup.""" + formatter = ConsoleFormatter() + + tree = Tree("Test Flow") + formatter.print(tree) + + assert formatter._live is not None + + formatter.stop_live() + + assert formatter._live is None + + with patch.object(formatter.console, 'print') as mock_print: + formatter.print("This should appear immediately") + mock_print.assert_called_once_with("This should appear immediately") + + def test_event_listener_cleanup_integration(self): + """Test that EventListener properly cleans up Live sessions.""" + event_listener = EventListener() + formatter = event_listener.formatter + + tree = Tree("Test Crew") + formatter.print(tree) + assert formatter._live is not None + + formatter.stop_live() + assert formatter._live is None + + def test_stop_live_restores_normal_output(self): + """Test that stop_live properly restores normal console output behavior.""" + formatter = ConsoleFormatter() + + tree = Tree("Test Tree") + formatter.print(tree) + + assert formatter._live is not None + + formatter.stop_live() + + assert formatter._live is None + + with patch.object(formatter.console, 'print') as mock_print: + formatter.print("Normal output") + mock_print.assert_called_once_with("Normal output") diff --git a/tests/utilities/test_console_formatter_pause_resume.py b/tests/utilities/test_console_formatter_pause_resume.py index e150671ac..1a1208310 100644 --- a/tests/utilities/test_console_formatter_pause_resume.py +++ b/tests/utilities/test_console_formatter_pause_resume.py @@ -114,3 +114,38 @@ class TestConsoleFormatterPauseResume: assert hasattr(formatter, '_live_paused') assert not formatter._live_paused + + def test_stop_live_with_active_session(self): + """Test stopping Live session when one is active.""" + formatter = ConsoleFormatter() + + mock_live = MagicMock(spec=Live) + formatter._live = mock_live + + formatter.stop_live() + + mock_live.stop.assert_called_once() + assert formatter._live is None + + def test_stop_live_with_no_session(self): + """Test stopping Live session when none exists.""" + formatter = ConsoleFormatter() + + formatter._live = None + + formatter.stop_live() + + assert formatter._live is None + + def test_stop_live_multiple_calls(self): + """Test multiple calls to stop_live are safe.""" + formatter = ConsoleFormatter() + + mock_live = MagicMock(spec=Live) + formatter._live = mock_live + + formatter.stop_live() + formatter.stop_live() + + mock_live.stop.assert_called_once() + assert formatter._live is None