diff --git a/openhands/runtime/utils/bash.py b/openhands/runtime/utils/bash.py index 4c35259db2..541c8cadf1 100644 --- a/openhands/runtime/utils/bash.py +++ b/openhands/runtime/utils/bash.py @@ -17,6 +17,7 @@ from openhands.events.observation.commands import ( CmdOutputMetadata, CmdOutputObservation, ) +from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE from openhands.utils.shutdown_listener import should_continue @@ -379,9 +380,7 @@ class BashSession: metadata = CmdOutputMetadata() # No metadata available metadata.suffix = ( f'\n[The command has no new output after {self.NO_CHANGE_TIMEOUT_SECONDS} seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' + f'{TIMEOUT_MESSAGE_TEMPLATE}]' ) command_output = self._get_command_output( command, @@ -414,9 +413,7 @@ class BashSession: metadata = CmdOutputMetadata() # No metadata available metadata.suffix = ( f'\n[The command timed out after {timeout} seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' + f'{TIMEOUT_MESSAGE_TEMPLATE}]' ) command_output = self._get_command_output( command, diff --git a/openhands/runtime/utils/bash_constants.py b/openhands/runtime/utils/bash_constants.py new file mode 100644 index 0000000000..3c3b73698b --- /dev/null +++ b/openhands/runtime/utils/bash_constants.py @@ -0,0 +1,7 @@ +# Common timeout message that can be used across different timeout scenarios +TIMEOUT_MESSAGE_TEMPLATE = ( + "You may wait longer to see additional output by sending empty command '', " + 'send other commands to interact with the current process, ' + 'send keys to interrupt/kill the command, ' + 'or use the timeout parameter in execute_bash for future commands.' +) diff --git a/openhands/runtime/utils/windows_bash.py b/openhands/runtime/utils/windows_bash.py index 06ed4464cc..d777e8c3f3 100644 --- a/openhands/runtime/utils/windows_bash.py +++ b/openhands/runtime/utils/windows_bash.py @@ -20,6 +20,7 @@ from openhands.events.observation.commands import ( CmdOutputMetadata, CmdOutputObservation, ) +from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE from openhands.utils.shutdown_listener import should_continue pythonnet.load('coreclr') @@ -559,9 +560,7 @@ class WindowsPowershellSession: else: metadata.suffix = ( f'\n[The command timed out after {timeout_seconds} seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' + f'{TIMEOUT_MESSAGE_TEMPLATE}]' ) return CmdOutputObservation( @@ -1331,9 +1330,7 @@ class WindowsPowershellSession: # Align suffix with bash.py timeout message suffix = ( f'\n[The command timed out after {timeout_seconds} seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' + f'{TIMEOUT_MESSAGE_TEMPLATE}]' ) elif shutdown_requested: # Align suffix with bash.py equivalent (though bash.py might not have specific shutdown message) diff --git a/tests/runtime/test_bash.py b/tests/runtime/test_bash.py index 83832b885c..0bdc50160e 100644 --- a/tests/runtime/test_bash.py +++ b/tests/runtime/test_bash.py @@ -16,6 +16,16 @@ from openhands.events.action import CmdRunAction from openhands.events.observation import CmdOutputObservation, ErrorObservation from openhands.runtime.impl.cli.cli_runtime import CLIRuntime from openhands.runtime.impl.local.local_runtime import LocalRuntime +from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE + + +def get_timeout_suffix(timeout_seconds): + """Helper function to generate the expected timeout suffix.""" + return ( + f'[The command timed out after {timeout_seconds} seconds. ' + f'{TIMEOUT_MESSAGE_TEMPLATE}]' + ) + # ============================================================================================================================ # Bash-specific tests @@ -56,10 +66,7 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands): if runtime_cls == CLIRuntime: assert '[The command timed out after 1.0 seconds.]' in obs.metadata.suffix else: - assert ( - "[The command timed out after 1.0 seconds. You may wait longer to see additional output by sending empty command '', send other commands to interact with the current process, or send keys to interrupt/kill the command.]" - in obs.metadata.suffix - ) + assert get_timeout_suffix(1.0) in obs.metadata.suffix action = CmdRunAction(command='C-c', is_input=True) action.set_hard_timeout(30) diff --git a/tests/runtime/trajs/basic_gui_mode.json b/tests/runtime/trajs/basic_gui_mode.json index a11dd473ae..3ffa3caf56 100644 --- a/tests/runtime/trajs/basic_gui_mode.json +++ b/tests/runtime/trajs/basic_gui_mode.json @@ -589,7 +589,7 @@ "working_dir": null, "py_interpreter_path": null, "prefix": "", - "suffix": "\n[The command has no new output after 30 seconds. You may wait longer to see additional output by sending empty command '', send other commands to interact with the current process, or send keys to interrupt/kill the command.]" + "suffix": "\n[The command has no new output after 30 seconds. You may wait longer to see additional output by sending empty command '', send other commands to interact with the current process, send keys to interrupt/kill the command, or use the timeout parameter in execute_bash for future commands.]" }, "hidden": false }, diff --git a/tests/unit/test_bash_session.py b/tests/unit/test_bash_session.py index cbd47f4ee7..6823d33d05 100644 --- a/tests/unit/test_bash_session.py +++ b/tests/unit/test_bash_session.py @@ -5,6 +5,15 @@ import time from openhands.core.logger import openhands_logger as logger from openhands.events.action import CmdRunAction from openhands.runtime.utils.bash import BashCommandStatus, BashSession +from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE + + +def get_no_change_timeout_suffix(timeout_seconds): + """Helper function to generate the expected no-change timeout suffix.""" + return ( + f'\n[The command has no new output after {timeout_seconds} seconds. ' + f'{TIMEOUT_MESSAGE_TEMPLATE}]' + ) def test_session_initialization(): @@ -83,12 +92,7 @@ def test_long_running_command_follow_by_execute(): assert '1' in obs.content # First number should appear before timeout assert obs.metadata.exit_code == -1 # -1 indicates command is still running assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT - assert obs.metadata.suffix == ( - '\n[The command has no new output after 2 seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' - ) + assert obs.metadata.suffix == get_no_change_timeout_suffix(2) assert obs.metadata.prefix == '' # Continue watching output @@ -96,12 +100,7 @@ def test_long_running_command_follow_by_execute(): logger.info(obs, extra={'msg_type': 'OBSERVATION'}) assert '2' in obs.content assert obs.metadata.prefix == '[Below is the output of the previous command.]\n' - assert obs.metadata.suffix == ( - '\n[The command has no new output after 2 seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' - ) + assert obs.metadata.suffix == get_no_change_timeout_suffix(2) assert obs.metadata.exit_code == -1 # -1 indicates command is still running assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT @@ -142,12 +141,7 @@ def test_interactive_command(): assert 'Enter name:' in obs.content assert obs.metadata.exit_code == -1 # -1 indicates command is still running assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT - assert obs.metadata.suffix == ( - '\n[The command has no new output after 3 seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' - ) + assert obs.metadata.suffix == get_no_change_timeout_suffix(3) assert obs.metadata.prefix == '' # Send input @@ -164,36 +158,21 @@ def test_interactive_command(): logger.info(obs, extra={'msg_type': 'OBSERVATION'}) assert obs.metadata.exit_code == -1 assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT - assert obs.metadata.suffix == ( - '\n[The command has no new output after 3 seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' - ) + assert obs.metadata.suffix == get_no_change_timeout_suffix(3) assert obs.metadata.prefix == '' obs = session.execute(CmdRunAction('line 1', is_input=True)) logger.info(obs, extra={'msg_type': 'OBSERVATION'}) assert obs.metadata.exit_code == -1 assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT - assert obs.metadata.suffix == ( - '\n[The command has no new output after 3 seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' - ) + assert obs.metadata.suffix == get_no_change_timeout_suffix(3) assert obs.metadata.prefix == '[Below is the output of the previous command.]\n' obs = session.execute(CmdRunAction('line 2', is_input=True)) logger.info(obs, extra={'msg_type': 'OBSERVATION'}) assert obs.metadata.exit_code == -1 assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT - assert obs.metadata.suffix == ( - '\n[The command has no new output after 3 seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' - ) + assert obs.metadata.suffix == get_no_change_timeout_suffix(3) assert obs.metadata.prefix == '[Below is the output of the previous command.]\n' obs = session.execute(CmdRunAction('EOF', is_input=True)) @@ -216,12 +195,7 @@ def test_ctrl_c(): ) logger.info(obs, extra={'msg_type': 'OBSERVATION'}) assert 'looping' in obs.content - assert obs.metadata.suffix == ( - '\n[The command has no new output after 2 seconds. ' - "You may wait longer to see additional output by sending empty command '', " - 'send other commands to interact with the current process, ' - 'or send keys to interrupt/kill the command.]' - ) + assert obs.metadata.suffix == get_no_change_timeout_suffix(2) assert obs.metadata.prefix == '' assert obs.metadata.exit_code == -1 # -1 indicates command is still running assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT diff --git a/tests/unit/test_windows_bash.py b/tests/unit/test_windows_bash.py index b9f1793f28..ae4e58488f 100644 --- a/tests/unit/test_windows_bash.py +++ b/tests/unit/test_windows_bash.py @@ -12,6 +12,16 @@ from openhands.events.observation import ErrorObservation from openhands.events.observation.commands import ( CmdOutputObservation, ) +from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE + + +def get_timeout_suffix(timeout_seconds): + """Helper function to generate the expected timeout suffix.""" + return ( + f'[The command timed out after {timeout_seconds} seconds. ' + f'{TIMEOUT_MESSAGE_TEMPLATE}]' + ) + # Skip all tests in this module if not running on Windows pytestmark = pytest.mark.skipif( @@ -168,10 +178,7 @@ def test_long_running_command(windows_bash_session): # Verify the initial output was captured assert 'Serving HTTP on' in result.content # Check for timeout specific metadata - assert ( - "[The command timed out after 1.0 seconds. You may wait longer to see additional output by sending empty command '', send other commands to interact with the current process, or send keys to interrupt/kill the command.]" - in result.metadata.suffix - ) + assert get_timeout_suffix(1.0) in result.metadata.suffix assert result.exit_code == -1 # The action timed out, but the command should be still running