mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Graham Neubig <neubig@gmail.com> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
import asyncio
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from litellm.exceptions import AuthenticationError
|
|
from pydantic import SecretStr
|
|
|
|
from openhands.cli import main as cli
|
|
from openhands.core.config.llm_config import LLMConfig
|
|
from openhands.events import EventSource
|
|
from openhands.events.action import MessageAction
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
def mock_agent():
|
|
agent = AsyncMock()
|
|
agent.reset = MagicMock()
|
|
return agent
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
def mock_runtime():
|
|
runtime = AsyncMock()
|
|
runtime.close = MagicMock()
|
|
runtime.event_stream = MagicMock()
|
|
return runtime
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
def mock_controller():
|
|
controller = AsyncMock()
|
|
controller.close = AsyncMock()
|
|
|
|
# Setup for get_state() and the returned state's save_to_session()
|
|
mock_state = MagicMock()
|
|
mock_state.save_to_session = MagicMock()
|
|
controller.get_state = MagicMock(return_value=mock_state)
|
|
return controller
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
def mock_config():
|
|
config = MagicMock()
|
|
config.runtime = 'local'
|
|
config.cli_multiline_input = False
|
|
config.workspace_base = '/test/dir'
|
|
|
|
# Set up LLM config to use OpenHands provider
|
|
llm_config = LLMConfig(model='openhands/o3', api_key=SecretStr('invalid-api-key'))
|
|
llm_config.model = 'openhands/o3' # Use OpenHands provider with o3 model
|
|
config.get_llm_config.return_value = llm_config
|
|
config.get_llm_config_from_agent.return_value = llm_config
|
|
|
|
# Mock search_api_key with get_secret_value method
|
|
search_api_key_mock = MagicMock()
|
|
search_api_key_mock.get_secret_value.return_value = (
|
|
'' # Empty string, not starting with 'tvly-'
|
|
)
|
|
config.search_api_key = search_api_key_mock
|
|
|
|
# Mock sandbox with volumes attribute to prevent finalize_config issues
|
|
config.sandbox = MagicMock()
|
|
config.sandbox.volumes = (
|
|
None # This prevents finalize_config from overriding workspace_base
|
|
)
|
|
|
|
return config
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
def mock_settings_store():
|
|
settings_store = AsyncMock()
|
|
return settings_store
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('openhands.cli.main.display_runtime_initialization_message')
|
|
@patch('openhands.cli.main.display_initialization_animation')
|
|
@patch('openhands.cli.main.create_agent')
|
|
@patch('openhands.cli.main.add_mcp_tools_to_agent')
|
|
@patch('openhands.cli.main.create_runtime')
|
|
@patch('openhands.cli.main.create_controller')
|
|
@patch('openhands.cli.main.create_memory')
|
|
@patch('openhands.cli.main.run_agent_until_done')
|
|
@patch('openhands.cli.main.cleanup_session')
|
|
@patch('openhands.cli.main.initialize_repository_for_runtime')
|
|
@patch('openhands.llm.llm.litellm_completion')
|
|
async def test_openhands_provider_authentication_error(
|
|
mock_litellm_completion,
|
|
mock_initialize_repo,
|
|
mock_cleanup_session,
|
|
mock_run_agent_until_done,
|
|
mock_create_memory,
|
|
mock_create_controller,
|
|
mock_create_runtime,
|
|
mock_add_mcp_tools,
|
|
mock_create_agent,
|
|
mock_display_animation,
|
|
mock_display_runtime_init,
|
|
mock_config,
|
|
mock_settings_store,
|
|
):
|
|
"""Test that authentication errors with the OpenHands provider are handled correctly.
|
|
|
|
This test reproduces the error seen in the CLI when using the OpenHands provider:
|
|
|
|
```
|
|
litellm.exceptions.AuthenticationError: litellm.AuthenticationError: AuthenticationError: Litellm_proxyException -
|
|
Authentication Error, Invalid proxy server token passed. Received API Key = sk-...7hlQ,
|
|
Key Hash (Token) =e316fa114498880be11f2e236d6f482feee5e324a4a148b98af247eded5290c4.
|
|
Unable to find token in cache or `LiteLLM_VerificationTokenTable`
|
|
|
|
18:38:53 - openhands:ERROR: loop.py:25 - STATUS$ERROR_LLM_AUTHENTICATION
|
|
```
|
|
|
|
The test mocks the litellm_completion function to raise an AuthenticationError
|
|
with the OpenHands provider and verifies that the CLI handles the error gracefully.
|
|
"""
|
|
loop = asyncio.get_running_loop()
|
|
|
|
# Mock initialize_repository_for_runtime to return a valid path
|
|
mock_initialize_repo.return_value = '/test/dir'
|
|
|
|
# Mock objects returned by the setup functions
|
|
mock_agent = AsyncMock()
|
|
mock_create_agent.return_value = mock_agent
|
|
|
|
mock_runtime = AsyncMock()
|
|
mock_runtime.event_stream = MagicMock()
|
|
mock_create_runtime.return_value = mock_runtime
|
|
|
|
mock_controller = AsyncMock()
|
|
mock_controller_task = MagicMock()
|
|
mock_create_controller.return_value = (mock_controller, mock_controller_task)
|
|
|
|
# Create a regular MagicMock for memory to avoid coroutine issues
|
|
mock_memory = MagicMock()
|
|
mock_create_memory.return_value = mock_memory
|
|
|
|
# Mock the litellm_completion function to raise an AuthenticationError
|
|
# This simulates the exact error seen in the user's issue
|
|
auth_error_message = (
|
|
'litellm.AuthenticationError: AuthenticationError: Litellm_proxyException - '
|
|
'Authentication Error, Invalid proxy server token passed. Received API Key = sk-...7hlQ, '
|
|
'Key Hash (Token) =e316fa114498880be11f2e236d6f482feee5e324a4a148b98af247eded5290c4. '
|
|
'Unable to find token in cache or `LiteLLM_VerificationTokenTable`'
|
|
)
|
|
mock_litellm_completion.side_effect = AuthenticationError(
|
|
message=auth_error_message, llm_provider='litellm_proxy', model='o3'
|
|
)
|
|
|
|
with patch(
|
|
'openhands.cli.main.read_prompt_input', new_callable=AsyncMock
|
|
) as mock_read_prompt:
|
|
# Set up read_prompt_input to return a string that will trigger the command handler
|
|
mock_read_prompt.return_value = '/exit'
|
|
|
|
# Mock handle_commands to return values that will exit the loop
|
|
with patch(
|
|
'openhands.cli.main.handle_commands', new_callable=AsyncMock
|
|
) as mock_handle_commands:
|
|
mock_handle_commands.return_value = (
|
|
True,
|
|
False,
|
|
False,
|
|
) # close_repl, reload_microagents, new_session_requested
|
|
|
|
# Mock logger.error to capture the error message
|
|
with patch('openhands.core.logger.openhands_logger.error'):
|
|
# Run the function with an initial action that will trigger the OpenHands provider
|
|
initial_action_content = 'Hello, I need help with a task'
|
|
|
|
# Run the function
|
|
result = await cli.run_session(
|
|
loop,
|
|
mock_config,
|
|
mock_settings_store,
|
|
'/test/dir',
|
|
initial_action_content,
|
|
)
|
|
|
|
# Check that an event was added to the event stream
|
|
mock_runtime.event_stream.add_event.assert_called_once()
|
|
call_args = mock_runtime.event_stream.add_event.call_args[0]
|
|
assert isinstance(call_args[0], MessageAction)
|
|
# The CLI might modify the initial message, so we don't check the exact content
|
|
assert call_args[1] == EventSource.USER
|
|
|
|
# Check that run_agent_until_done was called
|
|
mock_run_agent_until_done.assert_called_once()
|
|
|
|
# Since we're mocking the litellm_completion function to raise an AuthenticationError,
|
|
# we can verify that the error was handled by checking that the run_agent_until_done
|
|
# function was called and the session was cleaned up properly
|
|
|
|
# We can't directly check the error message in the test since the logger.error
|
|
# method isn't being called in our mocked environment. In a real environment,
|
|
# the error would be logged and the user would see the improved error message.
|
|
|
|
# Check that cleanup_session was called
|
|
mock_cleanup_session.assert_called_once()
|
|
|
|
# Check that the function returns the expected value
|
|
assert result is False
|