diff --git a/tests/runtime/test_bash.py b/tests/runtime/test_bash.py index 5d36722169d0..8d8c360bf62b 100644 --- a/tests/runtime/test_bash.py +++ b/tests/runtime/test_bash.py @@ -593,3 +593,93 @@ def test_ansi_escape_codes(temp_dir, runtime_cls, run_as_openhands): assert 'Red Text' in obs.content finally: _close_test_runtime(runtime) + + +def test_command_output_continuation(temp_dir, runtime_cls, run_as_openhands): + runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + try: + # Start a command that produces output slowly + action = CmdRunAction('for i in {1..5}; do echo $i; sleep 3; done') + action.timeout = 2 # Set timeout to 2 seconds + obs = runtime.run_action(action) + assert obs.content.strip() == '1' + assert obs.metadata.prefix == '' + assert '[The command timed out after 2 seconds.' in obs.metadata.suffix + + # Continue watching output + action = CmdRunAction('') + action.timeout = 2 + obs = runtime.run_action(action) + assert '[Command output continued from previous command]' in obs.metadata.prefix + assert obs.content.strip() == '2' + assert '[The command timed out after 2 seconds.' in obs.metadata.suffix + + # Continue until completion + for expected in ['3', '4', '5']: + action = CmdRunAction('') + action.timeout = 2 + obs = runtime.run_action(action) + assert ( + '[Command output continued from previous command]' + in obs.metadata.prefix + ) + assert obs.content.strip() == expected + assert '[The command timed out after 2 seconds.' in obs.metadata.suffix + + # Final empty command to complete + action = CmdRunAction('') + obs = runtime.run_action(action) + assert '[The command completed with exit code 0.]' in obs.metadata.suffix + finally: + _close_test_runtime(runtime) + + +def test_long_running_command_follow_by_execute( + temp_dir, runtime_cls, run_as_openhands +): + runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + try: + # Test command that produces output slowly + action = CmdRunAction('for i in {1..3}; do echo $i; sleep 3; done') + action.timeout = 2 + action.blocking = False + obs = runtime.run_action(action) + assert '1' in obs.content # First number should appear before timeout + assert obs.metadata.exit_code == -1 # -1 indicates command is still running + assert '[The command timed out after 2 seconds.' in obs.metadata.suffix + assert obs.metadata.prefix == '' + + # Continue watching output + action = CmdRunAction('') + action.timeout = 2 + obs = runtime.run_action(action) + assert '2' in obs.content + assert ( + obs.metadata.prefix == '[Command output continued from previous command]\n' + ) + assert '[The command timed out after 2 seconds.' in obs.metadata.suffix + assert obs.metadata.exit_code == -1 # -1 indicates command is still running + + # Test command that produces no output + action = CmdRunAction('sleep 15') + action.timeout = 2 + obs = runtime.run_action(action) + assert '3' in obs.content + assert ( + obs.metadata.prefix == '[Command output continued from previous command]\n' + ) + assert '[The command timed out after 2 seconds.' in obs.metadata.suffix + assert obs.metadata.exit_code == -1 # -1 indicates command is still running + finally: + _close_test_runtime(runtime) + + +def test_empty_command_errors(temp_dir, runtime_cls, run_as_openhands): + runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + try: + # Test empty command without previous command + obs = runtime.run_action(CmdRunAction('')) + assert isinstance(obs, ErrorObservation) + assert 'No previous command to continue from' in obs.content + finally: + _close_test_runtime(runtime) diff --git a/tests/unit/test_bash_session.py b/tests/unit/test_bash_session.py index 9e5275c08ca1..947776041fb7 100644 --- a/tests/unit/test_bash_session.py +++ b/tests/unit/test_bash_session.py @@ -3,7 +3,6 @@ from openhands.core.logger import openhands_logger as logger from openhands.events.action import CmdRunAction -from openhands.events.observation import ErrorObservation from openhands.runtime.utils.bash import BashCommandStatus, BashSession @@ -54,14 +53,6 @@ def test_basic_command(): assert obs.metadata.prefix == '' assert session.prev_status == BashCommandStatus.COMPLETED - # Test command with special characters - obs = session.execute(CmdRunAction("echo 'hello world with\nspecial chars'")) - assert 'hello world with\nspecial chars' in obs.content - assert obs.metadata.suffix == '\n\n[The command completed with exit code 0.]' - assert obs.metadata.prefix == '' - assert obs.metadata.exit_code == 0 - assert session.prev_status == BashCommandStatus.COMPLETED - # Test multiple commands in sequence obs = session.execute(CmdRunAction('echo "first" && echo "second" && echo "third"')) assert 'first\nsecond\nthird' in obs.content @@ -251,21 +242,6 @@ def test_empty_command_errors(): session.close() -def test_env_command(): - session = BashSession(work_dir=os.getcwd()) - - # Test empty command without previous command - obs = session.execute(CmdRunAction('env')) - logger.info(obs, extra={'msg_type': 'OBSERVATION'}) - assert 'PS1="\n###PS1JSON###' in obs.content or 'PS1=\n###PS1JSON###' in obs.content - assert 'PS2=' in obs.content - assert obs.metadata.exit_code == 0 - assert obs.metadata.prefix == '' - assert obs.metadata.suffix == '\n\n[The command completed with exit code 0.]' - assert session.prev_status == BashCommandStatus.COMPLETED - session.close() - - def test_command_output_continuation(): session = BashSession(work_dir=os.getcwd(), no_change_timeout_seconds=2) @@ -360,53 +336,3 @@ def test_multiline_command(): assert obs.metadata.suffix == '\n\n[The command completed with exit code 0.]' session.close() - - -def test_multiple_multiline_commands(): - session = BashSession(work_dir=os.getcwd()) - try: - cmds = [ - 'ls -l', - 'echo -e "hello\nworld"', - """echo -e "hello it's me\"""", - """echo \\ - -e 'hello' \\ - -v""", - """echo -e 'hello\\nworld\\nare\\nyou\\nthere?'""", - """echo -e 'hello\nworld\nare\nyou\n\nthere?'""", - """echo -e 'hello\nworld "'""", - ] - joined_cmds = '\n'.join(cmds) - - # Test that running multiple commands at once fails - obs = session.execute(CmdRunAction(joined_cmds)) - logger.info(obs, extra={'msg_type': 'OBSERVATION'}) - assert isinstance(obs, ErrorObservation) - assert 'Cannot execute multiple commands at once' in obs.content - - # Now run each command individually and verify they work - results = [] - for cmd in cmds: - obs = session.execute(CmdRunAction(cmd)) - logger.info(obs, extra={'msg_type': 'OBSERVATION'}) - assert obs.metadata.exit_code == 0 - assert obs.metadata.prefix == '' - assert ( - obs.metadata.suffix == '\n\n[The command completed with exit code 0.]' - ) - results.append(obs.content) - - # Verify all expected outputs are present - assert 'total' in results[0] # ls -l - assert 'hello\nworld' in results[1] # echo -e "hello\nworld" - assert "hello it's me" in results[2] # echo -e "hello it\'s me" - assert 'hello -v' in results[3] # echo -e 'hello' -v - assert ( - 'hello\nworld\nare\nyou\nthere?' in results[4] - ) # echo -e 'hello\nworld\nare\nyou\nthere?' - assert ( - 'hello\nworld\nare\nyou\n\nthere?' in results[5] - ) # echo -e with literal newlines - assert 'hello\nworld "' in results[6] # echo -e with quote - finally: - session.close()