OpenHands/tests/unit/agenthub/test_str_replace_editor_tool.py
2025-09-15 17:46:54 -07:00

226 lines
10 KiB
Python

"""Tests for str_replace_editor tool workspace path detection."""
import os
import sys
from unittest.mock import patch
import pytest
# Import the functions directly to avoid dependency issues
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
try:
from openhands.agenthub.codeact_agent.tools.str_replace_editor import (
_get_workspace_mount_path_from_env,
create_str_replace_editor_tool,
)
except ImportError:
# If import fails due to dependencies, skip these tests
pytest.skip(
'Cannot import str_replace_editor due to missing dependencies',
allow_module_level=True,
)
class TestWorkspacePathDetection:
"""Test workspace path detection logic."""
def test_docker_runtime_ignores_sandbox_volumes(self):
"""Test that DockerRuntime ignores SANDBOX_VOLUMES and uses /workspace."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
result = _get_workspace_mount_path_from_env(runtime_type='docker')
assert result == '/workspace'
def test_remote_runtime_ignores_sandbox_volumes(self):
"""Test that RemoteRuntime ignores SANDBOX_VOLUMES and uses /workspace."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
result = _get_workspace_mount_path_from_env(runtime_type='remote')
assert result == '/workspace'
def test_e2b_runtime_ignores_sandbox_volumes(self):
"""Test that E2BRuntime ignores SANDBOX_VOLUMES and uses /workspace."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
result = _get_workspace_mount_path_from_env(runtime_type='e2b')
assert result == '/workspace'
def test_local_runtime_uses_sandbox_volumes(self):
"""Test that LocalRuntime uses host path from SANDBOX_VOLUMES."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == '/host/app'
def test_local_runtime_multiple_mounts(self):
"""Test LocalRuntime with multiple mounts, finds /workspace mount."""
with patch.dict(
os.environ,
{
'SANDBOX_VOLUMES': '/tmp:/tmp:rw,/home/user/project:/workspace:rw,/var:/var:rw'
},
):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == '/home/user/project'
def test_cli_runtime_multiple_mounts(self):
"""Test CLIRuntime with multiple mounts, finds /workspace mount."""
with patch.dict(
os.environ,
{
'SANDBOX_VOLUMES': '/tmp:/tmp:rw,/home/user/project:/workspace:rw,/var:/var:rw'
},
):
result = _get_workspace_mount_path_from_env(runtime_type='cli')
assert result == '/home/user/project'
def test_local_runtime_no_workspace_mount(self):
"""Test LocalRuntime with no /workspace mount falls back to current directory."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/tmp:/tmp:rw,/var:/var:rw'}):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == os.getcwd()
def test_local_runtime_no_sandbox_volumes(self):
"""Test LocalRuntime with no SANDBOX_VOLUMES falls back to current directory."""
with patch.dict(os.environ, {}, clear=True):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == os.getcwd()
def test_local_runtime_empty_sandbox_volumes(self):
"""Test LocalRuntime with empty SANDBOX_VOLUMES falls back to current directory."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': ''}):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == os.getcwd()
def test_relative_path_conversion(self):
"""Test that relative paths in SANDBOX_VOLUMES are converted to absolute."""
with patch.dict(
os.environ, {'SANDBOX_VOLUMES': './relative/path:/workspace:rw'}
):
result = _get_workspace_mount_path_from_env(runtime_type='local')
# Should be converted to absolute path
assert os.path.isabs(result)
assert result.endswith('relative/path')
def test_unknown_runtime_type(self):
"""Test that unknown runtime types use default container path."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
result = _get_workspace_mount_path_from_env(runtime_type='unknown')
assert result == '/workspace'
def test_none_runtime_type(self):
"""Test that None runtime type uses default container path."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
result = _get_workspace_mount_path_from_env(runtime_type=None)
assert result == '/workspace'
class TestToolCreation:
"""Test tool creation with dynamic workspace paths."""
def test_tool_creation_docker_runtime(self):
"""Test tool creation in DockerRuntime shows /workspace in description."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
tool = create_str_replace_editor_tool(runtime_type='docker')
path_description = tool['function']['parameters']['properties']['path'][
'description'
]
assert '/workspace/file.py' in path_description
assert '/workspace`.' in path_description
# Should not contain host paths
assert '/host/app' not in path_description
def test_tool_creation_local_runtime(self):
"""Test tool creation in LocalRuntime shows host path in description."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
tool = create_str_replace_editor_tool(runtime_type='local')
path_description = tool['function']['parameters']['properties']['path'][
'description'
]
assert '/host/app/file.py' in path_description
assert '/host/app`.' in path_description
def test_tool_creation_cli_runtime(self):
"""Test tool creation in CLIRuntime shows host path in description."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
tool = create_str_replace_editor_tool(runtime_type='cli')
path_description = tool['function']['parameters']['properties']['path'][
'description'
]
assert '/host/app/file.py' in path_description
assert '/host/app`.' in path_description
def test_tool_creation_remote_runtime(self):
"""Test tool creation in RemoteRuntime shows /workspace in description."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
tool = create_str_replace_editor_tool(runtime_type='remote')
path_description = tool['function']['parameters']['properties']['path'][
'description'
]
assert '/workspace/file.py' in path_description
assert '/workspace`.' in path_description
# Should not contain host paths
assert '/host/app' not in path_description
def test_tool_creation_e2b_runtime(self):
"""Test tool creation in E2BRuntime shows /workspace in description."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
tool = create_str_replace_editor_tool(runtime_type='e2b')
path_description = tool['function']['parameters']['properties']['path'][
'description'
]
assert '/workspace/file.py' in path_description
assert '/workspace`.' in path_description
# Should not contain host paths
assert '/host/app' not in path_description
def test_tool_creation_explicit_workspace_path(self):
"""Test tool creation with explicitly provided workspace path."""
tool = create_str_replace_editor_tool(
workspace_mount_path_in_sandbox='/custom/path'
)
path_description = tool['function']['parameters']['properties']['path'][
'description'
]
assert '/custom/path/file.py' in path_description
assert '/custom/path`.' in path_description
def test_tool_creation_short_description(self):
"""Test tool creation with short description still has correct path."""
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:/workspace:rw'}):
tool = create_str_replace_editor_tool(
use_short_description=True, runtime_type='local'
)
path_description = tool['function']['parameters']['properties']['path'][
'description'
]
assert '/host/app/file.py' in path_description
assert '/host/app`.' in path_description
class TestEdgeCases:
"""Test edge cases and error conditions."""
def test_malformed_sandbox_volumes(self):
"""Test handling of malformed SANDBOX_VOLUMES."""
# Missing colon separator - falls back to current directory for local/CLI
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app/workspace'}):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == os.getcwd()
# Only one part - falls back to current directory for local/CLI
with patch.dict(os.environ, {'SANDBOX_VOLUMES': '/host/app:'}):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == os.getcwd()
def test_workspace_mount_with_different_permissions(self):
"""Test /workspace mount with different permission strings."""
test_cases = [
'/host/app:/workspace:ro',
'/host/app:/workspace:rw',
'/host/app:/workspace', # No permissions
'/host/app:/workspace:rw,size=100m', # Extra options
]
for sandbox_volumes in test_cases:
with patch.dict(os.environ, {'SANDBOX_VOLUMES': sandbox_volumes}):
result = _get_workspace_mount_path_from_env(runtime_type='local')
assert result == '/host/app', f'Failed for: {sandbox_volumes}'