OpenHands / tests /runtime /test_aci_edit.py
Backup-bdg's picture
Upload 964 files
51ff9e5 verified
raw
history blame
25.6 kB
"""Editor-related tests for the DockerRuntime."""
import os
from unittest.mock import MagicMock
from conftest import _close_test_runtime, _load_runtime
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import FileEditAction, FileWriteAction
from openhands.runtime.action_execution_server import _execute_file_editor
from openhands.runtime.impl.cli.cli_runtime import CLIRuntime
def test_view_file(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
)
obs = runtime.run_action(action)
# Test view command
action = FileEditAction(
command='view',
path=test_file,
)
obs = runtime.run_action(action)
assert f"Here's the result of running `cat -n` on {test_file}:" in obs.content
assert '1\tThis is a test file.' in obs.content
assert '2\tThis file is for testing purposes.' in obs.content
finally:
_close_test_runtime(runtime)
def test_view_directory(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
)
obs = runtime.run_action(action)
# Test view command
action = FileEditAction(
command='view',
path=config.workspace_mount_path_in_sandbox,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert (
obs.content
== f"""Here's the files and directories up to 2 levels deep in {config.workspace_mount_path_in_sandbox}, excluding hidden items:
{config.workspace_mount_path_in_sandbox}/
{config.workspace_mount_path_in_sandbox}/test.txt"""
)
finally:
_close_test_runtime(runtime)
def test_create_file(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
new_file = os.path.join(config.workspace_mount_path_in_sandbox, 'new_file.txt')
action = FileEditAction(
command='create',
path=new_file,
file_text='New file content',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'File created successfully' in obs.content
# Verify file content
action = FileEditAction(
command='view',
path=new_file,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'New file content' in obs.content
finally:
_close_test_runtime(runtime)
def test_create_file_with_empty_content(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
new_file = os.path.join(config.workspace_mount_path_in_sandbox, 'new_file.txt')
action = FileEditAction(
command='create',
path=new_file,
file_text='',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'File created successfully' in obs.content
# Verify file content
action = FileEditAction(
command='view',
path=new_file,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert '1\t' in obs.content
finally:
_close_test_runtime(runtime)
def test_create_with_none_file_text(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
new_file = os.path.join(
config.workspace_mount_path_in_sandbox, 'none_content.txt'
)
action = FileEditAction(
command='create',
path=new_file,
file_text=None,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert (
obs.content
== 'ERROR:\nParameter `file_text` is required for command: create.'
)
finally:
_close_test_runtime(runtime)
def test_str_replace(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
)
runtime.run_action(action)
# Test str_replace command
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='test file',
new_str='sample file',
)
obs = runtime.run_action(action)
assert f'The file {test_file} has been edited' in obs.content
# Verify file content
action = FileEditAction(
command='view',
path=test_file,
)
obs = runtime.run_action(action)
assert 'This is a sample file.' in obs.content
finally:
_close_test_runtime(runtime)
def test_str_replace_multi_line(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
)
runtime.run_action(action)
# Test str_replace command
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='This is a test file.\nThis file is for testing purposes.',
new_str='This is a sample file.\nThis file is for testing purposes.',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert f'The file {test_file} has been edited.' in obs.content
assert 'This is a sample file.' in obs.content
assert 'This file is for testing purposes.' in obs.content
finally:
_close_test_runtime(runtime)
def test_str_replace_multi_line_with_tabs(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileEditAction(
command='create',
path=test_file,
file_text='def test():\n\tprint("Hello, World!")',
)
runtime.run_action(action)
# Test str_replace command
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='def test():\n\tprint("Hello, World!")',
new_str='def test():\n\tprint("Hello, Universe!")',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert (
obs.content
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
1\tdef test():
2\t\tprint("Hello, Universe!")
Review the changes and make sure they are as expected. Edit the file again if necessary."""
)
finally:
_close_test_runtime(runtime)
def test_str_replace_error_multiple_occurrences(
temp_dir, runtime_cls, run_as_openhands
):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='This is a test file.\nThis file is for testing purposes.',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='str_replace', path=test_file, old_str='test', new_str='sample'
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'Multiple occurrences of old_str `test`' in obs.content
assert '[1, 2]' in obs.content # Should show both line numbers
finally:
_close_test_runtime(runtime)
def test_str_replace_error_multiple_multiline_occurrences(
temp_dir, runtime_cls, run_as_openhands
):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
# Create a file with two identical multi-line blocks
multi_block = """def example():
print("Hello")
return True"""
content = f"{multi_block}\n\nprint('separator')\n\n{multi_block}"
action = FileWriteAction(
content=content,
path=test_file,
)
runtime.run_action(action)
# Test str_replace command
action = FileEditAction(
command='str_replace',
path=test_file,
old_str=multi_block,
new_str='def new():\n print("World")',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'Multiple occurrences of old_str' in obs.content
assert '[1, 7]' in obs.content # Should show correct starting line numbers
finally:
_close_test_runtime(runtime)
def test_str_replace_nonexistent_string(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='Non-existent Line',
new_str='New Line',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'No replacement was performed' in obs.content
assert (
f'old_str `Non-existent Line` did not appear verbatim in {test_file}'
in obs.content
)
finally:
_close_test_runtime(runtime)
def test_str_replace_with_empty_new_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine to remove\nLine 3',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='Line to remove\n',
new_str='',
)
obs = runtime.run_action(action)
assert 'Line to remove' not in obs.content
assert 'Line 1' in obs.content
assert 'Line 3' in obs.content
finally:
_close_test_runtime(runtime)
def test_str_replace_with_empty_old_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2\nLine 3',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='',
new_str='New string',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if isinstance(runtime, CLIRuntime):
# CLIRuntime with a 3-line file without a trailing newline reports 3 occurrences for an empty old_str
assert (
'No replacement was performed. Multiple occurrences of old_str `` in lines [1, 2, 3]. Please ensure it is unique.'
in obs.content
)
else:
# Other runtimes might behave differently (e.g., implicitly add a newline, leading to 4 matches)
# TODO: Why do they have 4 lines?
assert (
'No replacement was performed. Multiple occurrences of old_str `` in lines [1, 2, 3, 4]. Please ensure it is unique.'
in obs.content
)
finally:
_close_test_runtime(runtime)
def test_str_replace_with_none_old_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2\nLine 3',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='str_replace',
path=test_file,
old_str=None,
new_str='new content',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'old_str' in obs.content
finally:
_close_test_runtime(runtime)
def test_insert(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
# Test insert command
action = FileEditAction(
command='insert',
path=test_file,
insert_line=1,
new_str='Inserted line',
)
obs = runtime.run_action(action)
assert f'The file {test_file} has been edited' in obs.content
# Verify file content
action = FileEditAction(
command='view',
path=test_file,
)
obs = runtime.run_action(action)
assert 'Line 1' in obs.content
assert 'Inserted line' in obs.content
assert 'Line 2' in obs.content
finally:
_close_test_runtime(runtime)
def test_insert_invalid_line(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='insert',
path=test_file,
insert_line=10,
new_str='Invalid Insert',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'Invalid `insert_line` parameter' in obs.content
assert 'It should be within the range of allowed values' in obs.content
finally:
_close_test_runtime(runtime)
def test_insert_with_empty_string(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='insert',
path=test_file,
insert_line=1,
new_str='',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert '1\tLine 1' in obs.content
assert '2\t\n' in obs.content
assert '3\tLine 2' in obs.content
finally:
_close_test_runtime(runtime)
def test_insert_with_none_new_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='insert',
path=test_file,
insert_line=1,
new_str=None,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'ERROR' in obs.content
assert 'Parameter `new_str` is required for command: insert' in obs.content
finally:
_close_test_runtime(runtime)
def test_undo_edit(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create test file
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='This is a test file.',
path=test_file,
)
runtime.run_action(action)
# Make an edit
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='test',
new_str='sample',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'This is a sample file.' in obs.content
# Undo the edit
action = FileEditAction(
command='undo_edit',
path=test_file,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'Last edit to' in obs.content
assert 'This is a test file.' in obs.content
# Verify file content
action = FileEditAction(
command='view',
path=test_file,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'This is a test file.' in obs.content
finally:
_close_test_runtime(runtime)
def test_validate_path_invalid(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
invalid_file = os.path.join(
config.workspace_mount_path_in_sandbox, 'nonexistent.txt'
)
action = FileEditAction(
command='view',
path=invalid_file,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'Invalid `path` parameter' in obs.content
assert f'The path {invalid_file} does not exist' in obs.content
finally:
_close_test_runtime(runtime)
def test_create_existing_file_error(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='create',
path=test_file,
file_text='New content',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'File already exists' in obs.content
finally:
_close_test_runtime(runtime)
def test_str_replace_missing_old_str(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='',
new_str='sample',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert (
'No replacement was performed. Multiple occurrences of old_str ``'
in obs.content
)
finally:
_close_test_runtime(runtime)
def test_str_replace_new_str_and_old_str_same(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='str_replace',
path=test_file,
old_str='test file',
new_str='test file',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert (
'No replacement was performed. `new_str` and `old_str` must be different.'
in obs.content
)
finally:
_close_test_runtime(runtime)
def test_insert_missing_line_param(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='Line 1\nLine 2',
path=test_file,
)
runtime.run_action(action)
action = FileEditAction(
command='insert',
path=test_file,
new_str='Missing insert line',
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'Parameter `insert_line` is required for command: insert' in obs.content
finally:
_close_test_runtime(runtime)
def test_undo_edit_no_history_error(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
empty_file = os.path.join(config.workspace_mount_path_in_sandbox, 'empty.txt')
action = FileWriteAction(
content='',
path=empty_file,
)
runtime.run_action(action)
action = FileEditAction(
command='undo_edit',
path=empty_file,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert 'No edit history found for' in obs.content
finally:
_close_test_runtime(runtime)
def test_view_large_file_with_truncation(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Create a large file to trigger truncation
large_file = os.path.join(
config.workspace_mount_path_in_sandbox, 'large_test.txt'
)
large_content = 'Line 1\n' * 16000 # 16000 lines should trigger truncation
action = FileWriteAction(
content=large_content,
path=large_file,
)
runtime.run_action(action)
action = FileEditAction(
command='view',
path=large_file,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert (
'Due to the max output limit, only part of this file has been shown to you.'
in obs.content
)
finally:
_close_test_runtime(runtime)
def test_insert_line_string_conversion():
"""Test that insert_line is properly converted from string to int.
This test reproduces issue #8369 Example 2 where a string value for insert_line
causes a TypeError in the editor.
"""
# Mock the OHEditor
mock_editor = MagicMock()
mock_editor.return_value = MagicMock(
error=None, output='Success', old_content=None, new_content=None
)
# Test with string insert_line
result, _ = _execute_file_editor(
editor=mock_editor,
command='insert',
path='/test/path.py',
insert_line='185', # String instead of int
new_str='test content',
)
# Verify the editor was called with the correct parameters (insert_line converted to int)
mock_editor.assert_called_once()
args, kwargs = mock_editor.call_args
assert isinstance(kwargs['insert_line'], int)
assert kwargs['insert_line'] == 185
assert result == 'Success'