diff --git a/openhands/cli/commands.py b/openhands/cli/commands.py index f44855a1c7..a95c80d8bc 100644 --- a/openhands/cli/commands.py +++ b/openhands/cli/commands.py @@ -127,6 +127,7 @@ async def handle_commands( config: OpenHandsConfig, current_dir: str, settings_store: FileSettingsStore, + agent_state: str, ) -> tuple[bool, bool, bool, ExitReason]: close_repl = False reload_microagents = False @@ -159,7 +160,9 @@ async def handle_commands( elif command == '/settings': await handle_settings_command(config, settings_store) elif command == '/resume': - close_repl, new_session_requested = await handle_resume_command(event_stream) + close_repl, new_session_requested = await handle_resume_command( + event_stream, agent_state + ) elif command == '/mcp': await handle_mcp_command(config) else: @@ -292,10 +295,20 @@ async def handle_settings_command( # This is a workaround to handle the resume command for the time being. Replace user message with the state change event once the issue is fixed. async def handle_resume_command( event_stream: EventStream, + agent_state: str, ) -> tuple[bool, bool]: close_repl = True new_session_requested = False + if agent_state != AgentState.PAUSED: + close_repl = False + print_formatted_text( + HTML( + 'Error: Agent is not paused. /resume command is only available when agent is paused.' + ) + ) + return close_repl, new_session_requested + event_stream.add_event( MessageAction(content='continue'), EventSource.USER, diff --git a/openhands/cli/main.py b/openhands/cli/main.py index 1038da61ba..647d4459a6 100644 --- a/openhands/cli/main.py +++ b/openhands/cli/main.py @@ -190,6 +190,7 @@ async def run_session( config, current_dir, settings_store, + agent_state, ) if close_repl: diff --git a/tests/unit/test_cli_commands.py b/tests/unit/test_cli_commands.py index 0089794831..84d1c5b61d 100644 --- a/tests/unit/test_cli_commands.py +++ b/tests/unit/test_cli_commands.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch import pytest +from prompt_toolkit.formatted_text import HTML from openhands.cli.commands import ( display_mcp_servers, @@ -32,6 +33,7 @@ class TestHandleCommands: config = MagicMock(spec=OpenHandsConfig) current_dir = '/test/dir' settings_store = MagicMock(spec=FileSettingsStore) + agent_state = AgentState.RUNNING return { 'event_stream': event_stream, @@ -40,6 +42,7 @@ class TestHandleCommands: 'config': config, 'current_dir': current_dir, 'settings_store': settings_store, + 'agent_state': agent_state, } @pytest.mark.asyncio @@ -562,13 +565,16 @@ class TestHandleSettingsCommand: class TestHandleResumeCommand: @pytest.mark.asyncio - async def test_handle_resume_command(self): - """Test that handle_resume_command adds the 'continue' message to the event stream.""" + @patch('openhands.cli.commands.print_formatted_text') + async def test_handle_resume_command_paused_state(self, mock_print): + """Test that handle_resume_command works when agent is in PAUSED state.""" # Create a mock event stream event_stream = MagicMock(spec=EventStream) - # Call the function - close_repl, new_session_requested = await handle_resume_command(event_stream) + # Call the function with PAUSED state + close_repl, new_session_requested = await handle_resume_command( + event_stream, AgentState.PAUSED + ) # Check that the event stream add_event was called with the correct message action event_stream.add_event.assert_called_once() @@ -583,6 +589,40 @@ class TestHandleResumeCommand: assert close_repl is True assert new_session_requested is False + # Verify no error message was printed + mock_print.assert_not_called() + + @pytest.mark.asyncio + @pytest.mark.parametrize( + 'invalid_state', [AgentState.RUNNING, AgentState.FINISHED, AgentState.ERROR] + ) + @patch('openhands.cli.commands.print_formatted_text') + async def test_handle_resume_command_invalid_states( + self, mock_print, invalid_state + ): + """Test that handle_resume_command shows error for all non-PAUSED states.""" + event_stream = MagicMock(spec=EventStream) + + close_repl, new_session_requested = await handle_resume_command( + event_stream, invalid_state + ) + + # Check that no event was added to the stream + event_stream.add_event.assert_not_called() + + # Verify print was called with the error message + assert mock_print.call_count == 1 + error_call = mock_print.call_args_list[0][0][0] + assert isinstance(error_call, HTML) + assert 'Error: Agent is not paused' in str(error_call) + assert '/resume command is only available when agent is paused' in str( + error_call + ) + + # Check the return values + assert close_repl is False + assert new_session_requested is False + class TestMCPErrorHandling: """Test MCP error handling in commands.""" diff --git a/tests/unit/test_cli_pause_resume.py b/tests/unit/test_cli_pause_resume.py index b10e38b474..fef7a8cb2b 100644 --- a/tests/unit/test_cli_pause_resume.py +++ b/tests/unit/test_cli_pause_resume.py @@ -248,6 +248,7 @@ class TestCliCommandsPauseResume: config = MagicMock() current_dir = '/test/dir' settings_store = MagicMock() + agent_state = AgentState.PAUSED # Mock return value mock_handle_resume.return_value = (False, False) @@ -266,10 +267,11 @@ class TestCliCommandsPauseResume: config, current_dir, settings_store, + agent_state, ) # Check that handle_resume_command was called with correct args - mock_handle_resume.assert_called_once_with(event_stream) + mock_handle_resume.assert_called_once_with(event_stream, agent_state) # Check the return values assert close_repl is False diff --git a/tests/unit/test_exit_reason.py b/tests/unit/test_exit_reason.py index 512fe13b28..8c862c4c91 100644 --- a/tests/unit/test_exit_reason.py +++ b/tests/unit/test_exit_reason.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock import pytest from openhands.cli.commands import handle_commands +from openhands.core.schema import AgentState from openhands.core.schema.exit_reason import ExitReason @@ -51,6 +52,7 @@ async def test_handle_exit_command_returns_intentional(monkeypatch): MagicMock(), '/tmp/test', MagicMock(), + AgentState.RUNNING, ) assert exit_reason == ExitReason.INTENTIONAL