mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
33 Commits
fix-basic-
...
fix-runtim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd1414f7d6 | ||
|
|
9d4b0cc29b | ||
|
|
c3e272cdf5 | ||
|
|
4c9c501ad0 | ||
|
|
1277b5a67c | ||
|
|
22e29885a1 | ||
|
|
b0a53e6ab5 | ||
|
|
716c1ec5b7 | ||
|
|
7ea2763fa2 | ||
|
|
ca2d9dece1 | ||
|
|
e450e126f9 | ||
|
|
3c79f06dfa | ||
|
|
79816cf582 | ||
|
|
93cce89313 | ||
|
|
6cb7066900 | ||
|
|
531603c391 | ||
|
|
20d51944a2 | ||
|
|
044cd4fbab | ||
|
|
3aa9f40fd3 | ||
|
|
cd12e465cd | ||
|
|
1c0d800041 | ||
|
|
d113abbd8b | ||
|
|
bd05a4b2e1 | ||
|
|
20cc2538e9 | ||
|
|
dbfc471490 | ||
|
|
922341c3f1 | ||
|
|
129989dd09 | ||
|
|
0a39bb83b1 | ||
|
|
dcf9e9f559 | ||
|
|
8246d6bcb8 | ||
|
|
301ddeb4e9 | ||
|
|
649acd3d9c | ||
|
|
4f33f0e35f |
@@ -58,6 +58,7 @@ class SandboxConfig(BaseModel):
|
||||
remote_runtime_init_timeout: int = Field(default=180)
|
||||
remote_runtime_api_timeout: int = Field(default=10)
|
||||
remote_runtime_enable_retries: bool = Field(default=True)
|
||||
retry_on_unrecoverable_runtime_error: bool = Field(default=False)
|
||||
remote_runtime_class: str | None = Field(
|
||||
default=None
|
||||
) # can be "None" (default to gvisor) or "sysbox" (support docker inside runtime + more stable)
|
||||
|
||||
@@ -14,10 +14,15 @@ from typing import Callable, cast
|
||||
from zipfile import ZipFile
|
||||
|
||||
import httpx
|
||||
import tenacity
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
||||
|
||||
from openhands.core.config import OpenHandsConfig, SandboxConfig
|
||||
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig
|
||||
from openhands.core.exceptions import AgentRuntimeDisconnectedError
|
||||
from openhands.core.exceptions import (
|
||||
AgentRuntimeDisconnectedError,
|
||||
AgentRuntimeUnavailableError,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events import EventSource, EventStream, EventStreamSubscriber
|
||||
from openhands.events.action import (
|
||||
@@ -333,22 +338,134 @@ class Runtime(FileEditRuntimeMixin):
|
||||
f'Failed export latest github token to runtime: {self.sid}, {e}'
|
||||
)
|
||||
|
||||
async def _handle_runtime_error(
|
||||
self,
|
||||
event: Action,
|
||||
error: Exception,
|
||||
retry_count: int,
|
||||
max_retries: int = 3,
|
||||
retry_delay: int = 10,
|
||||
) -> None:
|
||||
"""
|
||||
Handle runtime-related errors with retry logic.
|
||||
|
||||
Args:
|
||||
event: The action that caused the error
|
||||
error: The exception that was raised
|
||||
retry_count: Current retry attempt number
|
||||
max_retries: Maximum number of retry attempts
|
||||
retry_delay: Delay in seconds between retries
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
error_message = f'{type(error).__name__}: {str(error)}'
|
||||
self.log('error', f'Runtime error while running action: {error_message}')
|
||||
self.log('error', f'Problematic action: {str(event)}')
|
||||
|
||||
# Reset MCP stdio servers tracking when error happens
|
||||
if hasattr(self, '_last_updated_mcp_stdio_servers'):
|
||||
from openhands.core.config.mcp_config import MCPStdioServerConfig
|
||||
|
||||
self._last_updated_mcp_stdio_servers: list[MCPStdioServerConfig] = []
|
||||
self.log(
|
||||
'debug',
|
||||
'Reset _last_updated_mcp_stdio_servers to empty list due to runtime error',
|
||||
)
|
||||
|
||||
# Create error message for the observation
|
||||
error_content = (
|
||||
f'Your command may have consumed too much resources, and the previous runtime died. '
|
||||
f'You are connected to a new runtime container, all dependencies you have installed '
|
||||
f'outside /workspace may not be persisted. (Retry {retry_count} of {max_retries})'
|
||||
)
|
||||
|
||||
# Create an error observation
|
||||
observation = ErrorObservation(content=error_content)
|
||||
|
||||
# Add the observation to the event stream
|
||||
observation._cause = event.id # type: ignore[attr-defined]
|
||||
observation.tool_call_metadata = event.tool_call_metadata
|
||||
self.event_stream.add_event(observation, EventSource.ENVIRONMENT) # type: ignore[arg-type]
|
||||
|
||||
# Log the retry attempt
|
||||
self.log(
|
||||
'warning',
|
||||
f'Runtime error occurred. Retry {retry_count} of {max_retries}.',
|
||||
)
|
||||
|
||||
async def _execute_action_core(self, event: Action) -> Observation:
|
||||
"""
|
||||
Core logic for executing an action.
|
||||
|
||||
Args:
|
||||
event: The action to execute
|
||||
|
||||
Returns:
|
||||
The observation resulting from the action
|
||||
"""
|
||||
await self._export_latest_git_provider_tokens(event)
|
||||
if isinstance(event, MCPAction):
|
||||
observation: Observation = await self.call_tool_mcp(event)
|
||||
else:
|
||||
observation = await call_sync_from_async(self.run_action, event)
|
||||
return observation
|
||||
|
||||
async def _handle_action(self, event: Action) -> None:
|
||||
if event.timeout is None:
|
||||
# We don't block the command if this is a default timeout action
|
||||
event.set_hard_timeout(self.config.sandbox.timeout, blocking=False)
|
||||
assert event.timeout is not None
|
||||
|
||||
# Define a before_sleep callback for tenacity
|
||||
async def before_sleep_callback(retry_state: tenacity.RetryCallState) -> None:
|
||||
exception = retry_state.outcome.exception()
|
||||
if exception:
|
||||
await self._handle_runtime_error(
|
||||
event,
|
||||
exception,
|
||||
retry_state.attempt_number,
|
||||
max_retries=3,
|
||||
retry_delay=10,
|
||||
)
|
||||
|
||||
# Create a retry decorator based on configuration
|
||||
if self.config.sandbox.retry_on_unrecoverable_runtime_error:
|
||||
retry_decorator = retry(
|
||||
retry=retry_if_exception_type(
|
||||
(AgentRuntimeDisconnectedError, AgentRuntimeUnavailableError)
|
||||
),
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_fixed(10),
|
||||
before_sleep=before_sleep_callback,
|
||||
reraise=True,
|
||||
)
|
||||
execute_with_retry = retry_decorator(self._execute_action_core)
|
||||
else:
|
||||
# No retry if not enabled in config
|
||||
execute_with_retry = self._execute_action_core
|
||||
|
||||
try:
|
||||
await self._export_latest_git_provider_tokens(event)
|
||||
if isinstance(event, MCPAction):
|
||||
observation: Observation = await self.call_tool_mcp(event)
|
||||
else:
|
||||
observation = await call_sync_from_async(self.run_action, event)
|
||||
# Execute the action with retry if configured
|
||||
observation: Observation = await execute_with_retry(event)
|
||||
|
||||
# Set observation metadata
|
||||
observation._cause = event.id # type: ignore[attr-defined]
|
||||
observation.tool_call_metadata = event.tool_call_metadata
|
||||
|
||||
except (AgentRuntimeDisconnectedError, AgentRuntimeUnavailableError) as e:
|
||||
# This will only be reached if retries are disabled or all retries failed
|
||||
err_id = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
|
||||
error_message = f'{type(e).__name__}: {str(e)}'
|
||||
self.log('error', f'Runtime error while running action: {error_message}')
|
||||
self.log('error', f'Problematic action: {str(event)}')
|
||||
self.send_error_message(err_id, error_message)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
# Handle other exceptions
|
||||
err_id = ''
|
||||
if isinstance(e, httpx.NetworkError) or isinstance(
|
||||
e, AgentRuntimeDisconnectedError
|
||||
):
|
||||
if isinstance(e, httpx.NetworkError):
|
||||
err_id = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
|
||||
error_message = f'{type(e).__name__}: {str(e)}'
|
||||
self.log('error', f'Unexpected error while running action: {error_message}')
|
||||
@@ -356,9 +473,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
self.send_error_message(err_id, error_message)
|
||||
return
|
||||
|
||||
observation._cause = event.id # type: ignore[attr-defined]
|
||||
observation.tool_call_metadata = event.tool_call_metadata
|
||||
|
||||
# this might be unnecessary, since source should be set by the event stream when we're here
|
||||
source = event.source if event.source else EventSource.AGENT
|
||||
if isinstance(observation, NullObservation):
|
||||
|
||||
416
tests/unit/test_runtime_error_handling.py
Normal file
416
tests/unit/test_runtime_error_handling.py
Normal file
@@ -0,0 +1,416 @@
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from openhands.core.config.mcp_config import MCPStdioServerConfig
|
||||
from openhands.core.exceptions import (
|
||||
AgentRuntimeDisconnectedError,
|
||||
AgentRuntimeTimeoutError,
|
||||
)
|
||||
from openhands.events.action import CmdRunAction, MCPAction
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.observation import ErrorObservation, Observation
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class TestRuntimeErrorHandling:
|
||||
"""Tests for runtime error handling functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_runtime(self):
|
||||
"""Create a mock Runtime with necessary attributes."""
|
||||
runtime = Mock(spec=Runtime)
|
||||
runtime._last_updated_mcp_stdio_servers = [
|
||||
MCPStdioServerConfig(name='test-server-1', command='test-command-1'),
|
||||
MCPStdioServerConfig(name='test-server-2', command='test-command-2'),
|
||||
]
|
||||
runtime.log = Mock()
|
||||
runtime.event_stream = Mock()
|
||||
runtime.event_stream.add_event = AsyncMock()
|
||||
runtime.send_error_message = Mock()
|
||||
runtime.config = Mock()
|
||||
runtime.config.sandbox = Mock()
|
||||
return runtime
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_runtime_error_resets_mcp_servers(self, mock_runtime):
|
||||
"""Test that _handle_runtime_error resets _last_updated_mcp_stdio_servers."""
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Patch the id property to return a valid integer
|
||||
with patch(
|
||||
'openhands.events.action.commands.CmdRunAction.id',
|
||||
new_callable=Mock,
|
||||
return_value=12345,
|
||||
):
|
||||
# Call the error handling method directly
|
||||
await Runtime._handle_runtime_error(
|
||||
mock_runtime,
|
||||
action,
|
||||
AgentRuntimeTimeoutError('Runtime timeout'),
|
||||
retry_count=1,
|
||||
max_retries=3,
|
||||
)
|
||||
|
||||
# Verify _last_updated_mcp_stdio_servers was reset
|
||||
assert mock_runtime._last_updated_mcp_stdio_servers == []
|
||||
|
||||
# Verify log message was called
|
||||
mock_runtime.log.assert_any_call(
|
||||
'debug',
|
||||
'Reset _last_updated_mcp_stdio_servers to empty list due to runtime error',
|
||||
)
|
||||
|
||||
# Verify an error observation was added to the event stream
|
||||
mock_runtime.event_stream.add_event.assert_called_once()
|
||||
|
||||
# Get the observation that was added
|
||||
call_args = mock_runtime.event_stream.add_event.call_args[0]
|
||||
observation = call_args[0]
|
||||
source = call_args[1]
|
||||
|
||||
# Verify it's an ErrorObservation with the right source
|
||||
assert isinstance(observation, ErrorObservation)
|
||||
assert source == EventSource.ENVIRONMENT
|
||||
|
||||
# Verify the error message contains the standard runtime error text
|
||||
assert (
|
||||
'Your command may have consumed too much resources'
|
||||
in observation.content
|
||||
)
|
||||
assert 'Retry 1 of 3' in observation.content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_runtime_error_on_disconnected(self, mock_runtime):
|
||||
"""Test that _handle_runtime_error handles disconnected errors correctly."""
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Patch the id property to return a valid integer
|
||||
with patch(
|
||||
'openhands.events.action.commands.CmdRunAction.id',
|
||||
new_callable=Mock,
|
||||
return_value=12345,
|
||||
):
|
||||
# Call the error handling method directly
|
||||
await Runtime._handle_runtime_error(
|
||||
mock_runtime,
|
||||
action,
|
||||
AgentRuntimeDisconnectedError('Runtime disconnected'),
|
||||
retry_count=2,
|
||||
max_retries=3,
|
||||
)
|
||||
|
||||
# Verify _last_updated_mcp_stdio_servers was reset
|
||||
assert mock_runtime._last_updated_mcp_stdio_servers == []
|
||||
|
||||
# Verify log message was called
|
||||
mock_runtime.log.assert_any_call(
|
||||
'debug',
|
||||
'Reset _last_updated_mcp_stdio_servers to empty list due to runtime error',
|
||||
)
|
||||
|
||||
# Verify an error observation was added to the event stream
|
||||
mock_runtime.event_stream.add_event.assert_called_once()
|
||||
|
||||
# Get the observation that was added
|
||||
call_args = mock_runtime.event_stream.add_event.call_args[0]
|
||||
observation = call_args[0]
|
||||
source = call_args[1]
|
||||
|
||||
# Verify it's an ErrorObservation with the right source
|
||||
assert isinstance(observation, ErrorObservation)
|
||||
assert source == EventSource.ENVIRONMENT
|
||||
|
||||
# Verify the error message contains the standard runtime error text
|
||||
assert (
|
||||
'Your command may have consumed too much resources'
|
||||
in observation.content
|
||||
)
|
||||
assert 'Retry 2 of 3' in observation.content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_runtime_error_on_http_error(self, mock_runtime):
|
||||
"""Test that _handle_runtime_error handles HTTP errors correctly."""
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Create a mock response with a 502 status code
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 502
|
||||
|
||||
# Patch the id property to return a valid integer
|
||||
with patch(
|
||||
'openhands.events.action.commands.CmdRunAction.id',
|
||||
new_callable=Mock,
|
||||
return_value=12345,
|
||||
):
|
||||
# Call the error handling method directly
|
||||
await Runtime._handle_runtime_error(
|
||||
mock_runtime,
|
||||
action,
|
||||
httpx.HTTPStatusError(
|
||||
'Bad Gateway', request=Mock(), response=mock_response
|
||||
),
|
||||
retry_count=1,
|
||||
max_retries=3,
|
||||
)
|
||||
|
||||
# Verify _last_updated_mcp_stdio_servers was reset
|
||||
assert mock_runtime._last_updated_mcp_stdio_servers == []
|
||||
|
||||
# Verify log message was called
|
||||
mock_runtime.log.assert_any_call(
|
||||
'debug',
|
||||
'Reset _last_updated_mcp_stdio_servers to empty list due to runtime error',
|
||||
)
|
||||
|
||||
# Verify an error observation was added to the event stream
|
||||
mock_runtime.event_stream.add_event.assert_called_once()
|
||||
|
||||
# Get the observation that was added
|
||||
call_args = mock_runtime.event_stream.add_event.call_args[0]
|
||||
observation = call_args[0]
|
||||
source = call_args[1]
|
||||
|
||||
# Verify it's an ErrorObservation with the right source
|
||||
assert isinstance(observation, ErrorObservation)
|
||||
assert source == EventSource.ENVIRONMENT
|
||||
|
||||
# Verify the error message contains the standard runtime error text
|
||||
assert (
|
||||
'Your command may have consumed too much resources'
|
||||
in observation.content
|
||||
)
|
||||
assert 'Retry 1 of 3' in observation.content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_runtime_error_on_max_retries(self, mock_runtime):
|
||||
"""Test that _handle_runtime_error handles max retries correctly."""
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Patch the id property to return a valid integer
|
||||
with patch(
|
||||
'openhands.events.action.commands.CmdRunAction.id',
|
||||
new_callable=Mock,
|
||||
return_value=12345,
|
||||
):
|
||||
# Call the error handling method directly
|
||||
await Runtime._handle_runtime_error(
|
||||
mock_runtime,
|
||||
action,
|
||||
Exception('Generic error'),
|
||||
retry_count=3, # Same as max_retries
|
||||
max_retries=3,
|
||||
)
|
||||
|
||||
# Verify _last_updated_mcp_stdio_servers was reset
|
||||
assert mock_runtime._last_updated_mcp_stdio_servers == []
|
||||
|
||||
# Verify log message was called
|
||||
mock_runtime.log.assert_any_call(
|
||||
'debug',
|
||||
'Reset _last_updated_mcp_stdio_servers to empty list due to runtime error',
|
||||
)
|
||||
|
||||
# Verify an error observation was added to the event stream
|
||||
mock_runtime.event_stream.add_event.assert_called_once()
|
||||
|
||||
# Get the observation that was added
|
||||
call_args = mock_runtime.event_stream.add_event.call_args[0]
|
||||
observation = call_args[0]
|
||||
source = call_args[1]
|
||||
|
||||
# Verify it's an ErrorObservation with the right source
|
||||
assert isinstance(observation, ErrorObservation)
|
||||
assert source == EventSource.ENVIRONMENT
|
||||
|
||||
# Verify the error message contains the standard runtime error text
|
||||
assert (
|
||||
'Your command may have consumed too much resources'
|
||||
in observation.content
|
||||
)
|
||||
assert 'Retry 3 of 3' in observation.content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_action_core(self, mock_runtime):
|
||||
"""Test the _execute_action_core method."""
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
|
||||
# Mock the run_action method
|
||||
mock_observation = Mock(spec=Observation)
|
||||
mock_runtime.run_action = Mock(return_value=mock_observation)
|
||||
|
||||
# Patch the call_sync_from_async function
|
||||
with patch(
|
||||
'openhands.runtime.base.call_sync_from_async',
|
||||
return_value=mock_observation,
|
||||
):
|
||||
# Call the method
|
||||
result = await Runtime._execute_action_core(mock_runtime, action)
|
||||
|
||||
# Verify the result
|
||||
assert result == mock_observation
|
||||
|
||||
# Verify _export_latest_git_provider_tokens was called
|
||||
mock_runtime._export_latest_git_provider_tokens.assert_called_once_with(
|
||||
action
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_action_core_with_mcp_action(self, mock_runtime):
|
||||
"""Test the _execute_action_core method with an MCP action."""
|
||||
# Create an MCP action
|
||||
action = Mock(spec=MCPAction)
|
||||
|
||||
# Mock the call_tool_mcp method
|
||||
mock_observation = Mock(spec=Observation)
|
||||
mock_runtime.call_tool_mcp = AsyncMock(return_value=mock_observation)
|
||||
|
||||
# Call the method
|
||||
result = await Runtime._execute_action_core(mock_runtime, action)
|
||||
|
||||
# Verify the result
|
||||
assert result == mock_observation
|
||||
|
||||
# Verify _export_latest_git_provider_tokens was called
|
||||
mock_runtime._export_latest_git_provider_tokens.assert_called_once_with(action)
|
||||
|
||||
# Verify call_tool_mcp was called
|
||||
mock_runtime.call_tool_mcp.assert_called_once_with(action)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_action_with_retry_enabled(self, mock_runtime):
|
||||
"""Test _handle_action with retry enabled."""
|
||||
# Configure the mock runtime
|
||||
mock_runtime.config.sandbox.retry_on_unrecoverable_runtime_error = True
|
||||
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Mock the _execute_action_core method
|
||||
mock_observation = Mock(spec=Observation)
|
||||
mock_runtime._execute_action_core = AsyncMock(return_value=mock_observation)
|
||||
|
||||
# Since we can't easily mock the tenacity.retry decorator directly,
|
||||
# we'll test the behavior by checking that the right configuration is used
|
||||
# when retry_on_unrecoverable_runtime_error is True
|
||||
|
||||
# Call the method with a patched _execute_action_core
|
||||
await Runtime._handle_action(mock_runtime, action)
|
||||
|
||||
# Verify _execute_action_core was called
|
||||
mock_runtime._execute_action_core.assert_called_once_with(action)
|
||||
|
||||
# Verify the observation was processed correctly
|
||||
assert hasattr(mock_observation, '_cause')
|
||||
assert hasattr(mock_observation, 'tool_call_metadata')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_action_with_retry_disabled(self, mock_runtime):
|
||||
"""Test _handle_action with retry disabled."""
|
||||
# Configure the mock runtime
|
||||
mock_runtime.config.sandbox.retry_on_unrecoverable_runtime_error = False
|
||||
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Mock the _execute_action_core method
|
||||
mock_observation = Mock(spec=Observation)
|
||||
mock_runtime._execute_action_core = AsyncMock(return_value=mock_observation)
|
||||
|
||||
# Call the method
|
||||
await Runtime._handle_action(mock_runtime, action)
|
||||
|
||||
# Verify _execute_action_core was called
|
||||
mock_runtime._execute_action_core.assert_called_once_with(action)
|
||||
|
||||
# Verify the observation was added to the event stream
|
||||
assert mock_observation._cause == action.id
|
||||
assert mock_observation.tool_call_metadata == action.tool_call_metadata
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_action_with_runtime_error(self, mock_runtime):
|
||||
"""Test _handle_action when a runtime error occurs."""
|
||||
# Configure the mock runtime
|
||||
mock_runtime.config.sandbox.retry_on_unrecoverable_runtime_error = False
|
||||
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Mock the _execute_action_core method to raise an error
|
||||
error = AgentRuntimeDisconnectedError('Runtime disconnected')
|
||||
mock_runtime._execute_action_core = AsyncMock(side_effect=error)
|
||||
|
||||
# Call the method
|
||||
await Runtime._handle_action(mock_runtime, action)
|
||||
|
||||
# Verify _execute_action_core was called
|
||||
mock_runtime._execute_action_core.assert_called_once_with(action)
|
||||
|
||||
# Verify send_error_message was called
|
||||
mock_runtime.send_error_message.assert_called_once_with(
|
||||
'STATUS$ERROR_RUNTIME_DISCONNECTED',
|
||||
'AgentRuntimeDisconnectedError: Runtime disconnected',
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_action_with_other_exception(self, mock_runtime):
|
||||
"""Test _handle_action when a non-runtime error occurs."""
|
||||
# Configure the mock runtime
|
||||
mock_runtime.config.sandbox.retry_on_unrecoverable_runtime_error = False
|
||||
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Mock the _execute_action_core method to raise an error
|
||||
error = ValueError('Invalid value')
|
||||
mock_runtime._execute_action_core = AsyncMock(side_effect=error)
|
||||
|
||||
# Call the method
|
||||
await Runtime._handle_action(mock_runtime, action)
|
||||
|
||||
# Verify _execute_action_core was called
|
||||
mock_runtime._execute_action_core.assert_called_once_with(action)
|
||||
|
||||
# Verify send_error_message was called
|
||||
mock_runtime.send_error_message.assert_called_once_with(
|
||||
'', 'ValueError: Invalid value'
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_action_with_network_error(self, mock_runtime):
|
||||
"""Test _handle_action when a network error occurs."""
|
||||
# Configure the mock runtime
|
||||
mock_runtime.config.sandbox.retry_on_unrecoverable_runtime_error = False
|
||||
|
||||
# Create a command action
|
||||
action = CmdRunAction(command='test command')
|
||||
action.set_hard_timeout(120)
|
||||
|
||||
# Mock the _execute_action_core method to raise an error
|
||||
error = httpx.NetworkError('Connection error')
|
||||
mock_runtime._execute_action_core = AsyncMock(side_effect=error)
|
||||
|
||||
# Call the method
|
||||
await Runtime._handle_action(mock_runtime, action)
|
||||
|
||||
# Verify _execute_action_core was called
|
||||
mock_runtime._execute_action_core.assert_called_once_with(action)
|
||||
|
||||
# Verify send_error_message was called
|
||||
mock_runtime.send_error_message.assert_called_once_with(
|
||||
'STATUS$ERROR_RUNTIME_DISCONNECTED', 'NetworkError: Connection error'
|
||||
)
|
||||
Reference in New Issue
Block a user