"""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'