mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 13:52:43 +08:00
499 lines
18 KiB
Python
499 lines
18 KiB
Python
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from openhands.cli.commands import (
|
|
handle_commands,
|
|
handle_exit_command,
|
|
handle_help_command,
|
|
handle_init_command,
|
|
handle_new_command,
|
|
handle_resume_command,
|
|
handle_settings_command,
|
|
handle_status_command,
|
|
)
|
|
from openhands.cli.tui import UsageMetrics
|
|
from openhands.core.config import OpenHandsConfig
|
|
from openhands.core.schema import AgentState
|
|
from openhands.events import EventSource
|
|
from openhands.events.action import ChangeAgentStateAction, MessageAction
|
|
from openhands.events.stream import EventStream
|
|
from openhands.storage.settings.file_settings_store import FileSettingsStore
|
|
|
|
|
|
class TestHandleCommands:
|
|
@pytest.fixture
|
|
def mock_dependencies(self):
|
|
event_stream = MagicMock(spec=EventStream)
|
|
usage_metrics = MagicMock(spec=UsageMetrics)
|
|
sid = 'test-session-id'
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
current_dir = '/test/dir'
|
|
settings_store = MagicMock(spec=FileSettingsStore)
|
|
|
|
return {
|
|
'event_stream': event_stream,
|
|
'usage_metrics': usage_metrics,
|
|
'sid': sid,
|
|
'config': config,
|
|
'current_dir': current_dir,
|
|
'settings_store': settings_store,
|
|
}
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.handle_exit_command')
|
|
async def test_handle_exit_command(self, mock_handle_exit, mock_dependencies):
|
|
mock_handle_exit.return_value = True
|
|
|
|
close_repl, reload_microagents, new_session = await handle_commands(
|
|
'/exit', **mock_dependencies
|
|
)
|
|
|
|
mock_handle_exit.assert_called_once_with(
|
|
mock_dependencies['config'],
|
|
mock_dependencies['event_stream'],
|
|
mock_dependencies['usage_metrics'],
|
|
mock_dependencies['sid'],
|
|
)
|
|
assert close_repl is True
|
|
assert reload_microagents is False
|
|
assert new_session is False
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.handle_help_command')
|
|
async def test_handle_help_command(self, mock_handle_help, mock_dependencies):
|
|
mock_handle_help.return_value = (False, False, False)
|
|
|
|
close_repl, reload_microagents, new_session = await handle_commands(
|
|
'/help', **mock_dependencies
|
|
)
|
|
|
|
mock_handle_help.assert_called_once()
|
|
assert close_repl is False
|
|
assert reload_microagents is False
|
|
assert new_session is False
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.handle_init_command')
|
|
async def test_handle_init_command(self, mock_handle_init, mock_dependencies):
|
|
mock_handle_init.return_value = (True, True)
|
|
|
|
close_repl, reload_microagents, new_session = await handle_commands(
|
|
'/init', **mock_dependencies
|
|
)
|
|
|
|
mock_handle_init.assert_called_once_with(
|
|
mock_dependencies['config'],
|
|
mock_dependencies['event_stream'],
|
|
mock_dependencies['current_dir'],
|
|
)
|
|
assert close_repl is True
|
|
assert reload_microagents is True
|
|
assert new_session is False
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.handle_status_command')
|
|
async def test_handle_status_command(self, mock_handle_status, mock_dependencies):
|
|
mock_handle_status.return_value = (False, False, False)
|
|
|
|
close_repl, reload_microagents, new_session = await handle_commands(
|
|
'/status', **mock_dependencies
|
|
)
|
|
|
|
mock_handle_status.assert_called_once_with(
|
|
mock_dependencies['usage_metrics'], mock_dependencies['sid']
|
|
)
|
|
assert close_repl is False
|
|
assert reload_microagents is False
|
|
assert new_session is False
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.handle_new_command')
|
|
async def test_handle_new_command(self, mock_handle_new, mock_dependencies):
|
|
mock_handle_new.return_value = (True, True)
|
|
|
|
close_repl, reload_microagents, new_session = await handle_commands(
|
|
'/new', **mock_dependencies
|
|
)
|
|
|
|
mock_handle_new.assert_called_once_with(
|
|
mock_dependencies['config'],
|
|
mock_dependencies['event_stream'],
|
|
mock_dependencies['usage_metrics'],
|
|
mock_dependencies['sid'],
|
|
)
|
|
assert close_repl is True
|
|
assert reload_microagents is False
|
|
assert new_session is True
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.handle_settings_command')
|
|
async def test_handle_settings_command(
|
|
self, mock_handle_settings, mock_dependencies
|
|
):
|
|
close_repl, reload_microagents, new_session = await handle_commands(
|
|
'/settings', **mock_dependencies
|
|
)
|
|
|
|
mock_handle_settings.assert_called_once_with(
|
|
mock_dependencies['config'],
|
|
mock_dependencies['settings_store'],
|
|
)
|
|
assert close_repl is False
|
|
assert reload_microagents is False
|
|
assert new_session is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_unknown_command(self, mock_dependencies):
|
|
user_message = 'Hello, this is not a command'
|
|
|
|
close_repl, reload_microagents, new_session = await handle_commands(
|
|
user_message, **mock_dependencies
|
|
)
|
|
|
|
# The command should be treated as a message and added to the event stream
|
|
mock_dependencies['event_stream'].add_event.assert_called_once()
|
|
# Check the first argument is a MessageAction with the right content
|
|
args, kwargs = mock_dependencies['event_stream'].add_event.call_args
|
|
assert isinstance(args[0], MessageAction)
|
|
assert args[0].content == user_message
|
|
assert args[1] == EventSource.USER
|
|
|
|
assert close_repl is True
|
|
assert reload_microagents is False
|
|
assert new_session is False
|
|
|
|
|
|
class TestHandleExitCommand:
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.display_shutdown_message')
|
|
def test_exit_with_confirmation(self, mock_display_shutdown, mock_cli_confirm):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
event_stream = MagicMock(spec=EventStream)
|
|
usage_metrics = MagicMock(spec=UsageMetrics)
|
|
sid = 'test-session-id'
|
|
|
|
# Mock user confirming exit
|
|
mock_cli_confirm.return_value = 0 # First option, which is "Yes, proceed"
|
|
|
|
# Call the function under test
|
|
result = handle_exit_command(config, event_stream, usage_metrics, sid)
|
|
|
|
# Verify correct behavior
|
|
mock_cli_confirm.assert_called_once()
|
|
event_stream.add_event.assert_called_once()
|
|
# Check event is the right type
|
|
args, kwargs = event_stream.add_event.call_args
|
|
assert isinstance(args[0], ChangeAgentStateAction)
|
|
assert args[0].agent_state == AgentState.STOPPED
|
|
assert args[1] == EventSource.ENVIRONMENT
|
|
|
|
mock_display_shutdown.assert_called_once_with(usage_metrics, sid)
|
|
assert result is True
|
|
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.display_shutdown_message')
|
|
def test_exit_without_confirmation(self, mock_display_shutdown, mock_cli_confirm):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
event_stream = MagicMock(spec=EventStream)
|
|
usage_metrics = MagicMock(spec=UsageMetrics)
|
|
sid = 'test-session-id'
|
|
|
|
# Mock user rejecting exit
|
|
mock_cli_confirm.return_value = 1 # Second option, which is "No, dismiss"
|
|
|
|
# Call the function under test
|
|
result = handle_exit_command(config, event_stream, usage_metrics, sid)
|
|
|
|
# Verify correct behavior
|
|
mock_cli_confirm.assert_called_once()
|
|
event_stream.add_event.assert_not_called()
|
|
mock_display_shutdown.assert_not_called()
|
|
assert result is False
|
|
|
|
|
|
class TestHandleHelpCommand:
|
|
@patch('openhands.cli.commands.display_help')
|
|
def test_help_command(self, mock_display_help):
|
|
handle_help_command()
|
|
mock_display_help.assert_called_once()
|
|
|
|
|
|
class TestHandleStatusCommand:
|
|
@patch('openhands.cli.commands.display_status')
|
|
def test_status_command(self, mock_display_status):
|
|
usage_metrics = MagicMock(spec=UsageMetrics)
|
|
sid = 'test-session-id'
|
|
|
|
handle_status_command(usage_metrics, sid)
|
|
|
|
mock_display_status.assert_called_once_with(usage_metrics, sid)
|
|
|
|
|
|
class TestHandleNewCommand:
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.display_shutdown_message')
|
|
def test_new_with_confirmation(self, mock_display_shutdown, mock_cli_confirm):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
event_stream = MagicMock(spec=EventStream)
|
|
usage_metrics = MagicMock(spec=UsageMetrics)
|
|
sid = 'test-session-id'
|
|
|
|
# Mock user confirming new session
|
|
mock_cli_confirm.return_value = 0 # First option, which is "Yes, proceed"
|
|
|
|
# Call the function under test
|
|
close_repl, new_session = handle_new_command(
|
|
config, event_stream, usage_metrics, sid
|
|
)
|
|
|
|
# Verify correct behavior
|
|
mock_cli_confirm.assert_called_once()
|
|
event_stream.add_event.assert_called_once()
|
|
# Check event is the right type
|
|
args, kwargs = event_stream.add_event.call_args
|
|
assert isinstance(args[0], ChangeAgentStateAction)
|
|
assert args[0].agent_state == AgentState.STOPPED
|
|
assert args[1] == EventSource.ENVIRONMENT
|
|
|
|
mock_display_shutdown.assert_called_once_with(usage_metrics, sid)
|
|
assert close_repl is True
|
|
assert new_session is True
|
|
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.display_shutdown_message')
|
|
def test_new_without_confirmation(self, mock_display_shutdown, mock_cli_confirm):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
event_stream = MagicMock(spec=EventStream)
|
|
usage_metrics = MagicMock(spec=UsageMetrics)
|
|
sid = 'test-session-id'
|
|
|
|
# Mock user rejecting new session
|
|
mock_cli_confirm.return_value = 1 # Second option, which is "No, dismiss"
|
|
|
|
# Call the function under test
|
|
close_repl, new_session = handle_new_command(
|
|
config, event_stream, usage_metrics, sid
|
|
)
|
|
|
|
# Verify correct behavior
|
|
mock_cli_confirm.assert_called_once()
|
|
event_stream.add_event.assert_not_called()
|
|
mock_display_shutdown.assert_not_called()
|
|
assert close_repl is False
|
|
assert new_session is False
|
|
|
|
|
|
class TestHandleInitCommand:
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.init_repository')
|
|
async def test_init_local_runtime_successful(self, mock_init_repository):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
config.runtime = 'local'
|
|
event_stream = MagicMock(spec=EventStream)
|
|
current_dir = '/test/dir'
|
|
|
|
# Mock successful repository initialization
|
|
mock_init_repository.return_value = True
|
|
|
|
# Call the function under test
|
|
close_repl, reload_microagents = await handle_init_command(
|
|
config, event_stream, current_dir
|
|
)
|
|
|
|
# Verify correct behavior
|
|
mock_init_repository.assert_called_once_with(config, current_dir)
|
|
event_stream.add_event.assert_called_once()
|
|
# Check event is the right type
|
|
args, kwargs = event_stream.add_event.call_args
|
|
assert isinstance(args[0], MessageAction)
|
|
assert 'Please explore this repository' in args[0].content
|
|
assert args[1] == EventSource.USER
|
|
|
|
assert close_repl is True
|
|
assert reload_microagents is True
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.init_repository')
|
|
async def test_init_local_runtime_unsuccessful(self, mock_init_repository):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
config.runtime = 'local'
|
|
event_stream = MagicMock(spec=EventStream)
|
|
current_dir = '/test/dir'
|
|
|
|
# Mock unsuccessful repository initialization
|
|
mock_init_repository.return_value = False
|
|
|
|
# Call the function under test
|
|
close_repl, reload_microagents = await handle_init_command(
|
|
config, event_stream, current_dir
|
|
)
|
|
|
|
# Verify correct behavior
|
|
mock_init_repository.assert_called_once_with(config, current_dir)
|
|
event_stream.add_event.assert_not_called()
|
|
|
|
assert close_repl is False
|
|
assert reload_microagents is False
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.print_formatted_text')
|
|
@patch('openhands.cli.commands.init_repository')
|
|
async def test_init_non_local_runtime(self, mock_init_repository, mock_print):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
config.runtime = 'remote' # Not local
|
|
event_stream = MagicMock(spec=EventStream)
|
|
current_dir = '/test/dir'
|
|
|
|
# Call the function under test
|
|
close_repl, reload_microagents = await handle_init_command(
|
|
config, event_stream, current_dir
|
|
)
|
|
|
|
# Verify correct behavior
|
|
mock_init_repository.assert_not_called()
|
|
mock_print.assert_called_once()
|
|
event_stream.add_event.assert_not_called()
|
|
|
|
assert close_repl is False
|
|
assert reload_microagents is False
|
|
|
|
|
|
class TestHandleSettingsCommand:
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.display_settings')
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.modify_llm_settings_basic')
|
|
async def test_settings_basic_with_changes(
|
|
self,
|
|
mock_modify_basic,
|
|
mock_cli_confirm,
|
|
mock_display_settings,
|
|
):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
settings_store = MagicMock(spec=FileSettingsStore)
|
|
|
|
# Mock user selecting "Basic" settings
|
|
mock_cli_confirm.return_value = 0
|
|
|
|
# Call the function under test
|
|
await handle_settings_command(config, settings_store)
|
|
|
|
# Verify correct behavior
|
|
mock_display_settings.assert_called_once_with(config)
|
|
mock_cli_confirm.assert_called_once()
|
|
mock_modify_basic.assert_called_once_with(config, settings_store)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.display_settings')
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.modify_llm_settings_basic')
|
|
async def test_settings_basic_without_changes(
|
|
self,
|
|
mock_modify_basic,
|
|
mock_cli_confirm,
|
|
mock_display_settings,
|
|
):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
settings_store = MagicMock(spec=FileSettingsStore)
|
|
|
|
# Mock user selecting "Basic" settings
|
|
mock_cli_confirm.return_value = 0
|
|
|
|
# Call the function under test
|
|
await handle_settings_command(config, settings_store)
|
|
|
|
# Verify correct behavior
|
|
mock_display_settings.assert_called_once_with(config)
|
|
mock_cli_confirm.assert_called_once()
|
|
mock_modify_basic.assert_called_once_with(config, settings_store)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.display_settings')
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.modify_llm_settings_advanced')
|
|
async def test_settings_advanced_with_changes(
|
|
self,
|
|
mock_modify_advanced,
|
|
mock_cli_confirm,
|
|
mock_display_settings,
|
|
):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
settings_store = MagicMock(spec=FileSettingsStore)
|
|
|
|
# Mock user selecting "Advanced" settings
|
|
mock_cli_confirm.return_value = 1
|
|
|
|
# Call the function under test
|
|
await handle_settings_command(config, settings_store)
|
|
|
|
# Verify correct behavior
|
|
mock_display_settings.assert_called_once_with(config)
|
|
mock_cli_confirm.assert_called_once()
|
|
mock_modify_advanced.assert_called_once_with(config, settings_store)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.display_settings')
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
@patch('openhands.cli.commands.modify_llm_settings_advanced')
|
|
async def test_settings_advanced_without_changes(
|
|
self,
|
|
mock_modify_advanced,
|
|
mock_cli_confirm,
|
|
mock_display_settings,
|
|
):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
settings_store = MagicMock(spec=FileSettingsStore)
|
|
|
|
# Mock user selecting "Advanced" settings
|
|
mock_cli_confirm.return_value = 1
|
|
|
|
# Call the function under test
|
|
await handle_settings_command(config, settings_store)
|
|
|
|
# Verify correct behavior
|
|
mock_display_settings.assert_called_once_with(config)
|
|
mock_cli_confirm.assert_called_once()
|
|
mock_modify_advanced.assert_called_once_with(config, settings_store)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.commands.display_settings')
|
|
@patch('openhands.cli.commands.cli_confirm')
|
|
async def test_settings_go_back(self, mock_cli_confirm, mock_display_settings):
|
|
config = MagicMock(spec=OpenHandsConfig)
|
|
settings_store = MagicMock(spec=FileSettingsStore)
|
|
|
|
# Mock user selecting "Go back"
|
|
mock_cli_confirm.return_value = 2
|
|
|
|
# Call the function under test
|
|
await handle_settings_command(config, settings_store)
|
|
|
|
# Verify correct behavior
|
|
mock_display_settings.assert_called_once_with(config)
|
|
mock_cli_confirm.assert_called_once()
|
|
|
|
|
|
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."""
|
|
# Create a mock event stream
|
|
event_stream = MagicMock(spec=EventStream)
|
|
|
|
# Call the function
|
|
close_repl, new_session_requested = await handle_resume_command(event_stream)
|
|
|
|
# Check that the event stream add_event was called with the correct message action
|
|
event_stream.add_event.assert_called_once()
|
|
args, kwargs = event_stream.add_event.call_args
|
|
message_action, source = args
|
|
|
|
assert isinstance(message_action, MessageAction)
|
|
assert message_action.content == 'continue'
|
|
assert source == EventSource.USER
|
|
|
|
# Check the return values
|
|
assert close_repl is True
|
|
assert new_session_requested is False
|