feat(cli): add agent state validation to /resume command (#10066)

This commit is contained in:
aeft 2025-08-04 12:14:21 -07:00 committed by GitHub
parent d233e89873
commit a36d1673fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 64 additions and 6 deletions

View File

@ -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(
'<ansired>Error: Agent is not paused. /resume command is only available when agent is paused.</ansired>'
)
)
return close_repl, new_session_requested
event_stream.add_event(
MessageAction(content='continue'),
EventSource.USER,

View File

@ -190,6 +190,7 @@ async def run_session(
config,
current_dir,
settings_store,
agent_state,
)
if close_repl:

View File

@ -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."""

View File

@ -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

View File

@ -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