test_ipython.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. """Test the EventStreamRuntime, which connects to the ActionExecutor running in the sandbox."""
  2. import pytest
  3. from conftest import (
  4. TEST_IN_CI,
  5. _close_test_runtime,
  6. _get_sandbox_folder,
  7. _load_runtime,
  8. )
  9. from openhands.core.logger import openhands_logger as logger
  10. from openhands.events.action import (
  11. CmdRunAction,
  12. FileReadAction,
  13. FileWriteAction,
  14. IPythonRunCellAction,
  15. )
  16. from openhands.events.observation import (
  17. CmdOutputObservation,
  18. ErrorObservation,
  19. FileReadObservation,
  20. FileWriteObservation,
  21. IPythonRunCellObservation,
  22. )
  23. # ============================================================================================================================
  24. # ipython-specific tests
  25. # ============================================================================================================================
  26. def test_simple_cmd_ipython_and_fileop(temp_dir, runtime_cls, run_as_openhands):
  27. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  28. sandbox_dir = _get_sandbox_folder(runtime)
  29. # Test run command
  30. action_cmd = CmdRunAction(command='ls -l')
  31. logger.info(action_cmd, extra={'msg_type': 'ACTION'})
  32. obs = runtime.run_action(action_cmd)
  33. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  34. assert isinstance(obs, CmdOutputObservation)
  35. assert obs.exit_code == 0
  36. assert 'total 0' in obs.content
  37. # Test run ipython
  38. test_code = "print('Hello, `World`!\\n')"
  39. action_ipython = IPythonRunCellAction(code=test_code)
  40. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  41. obs = runtime.run_action(action_ipython)
  42. assert isinstance(obs, IPythonRunCellObservation)
  43. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  44. assert obs.content.strip() == (
  45. 'Hello, `World`!\n'
  46. f'[Jupyter current working directory: {sandbox_dir}]\n'
  47. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  48. )
  49. # Test read file (file should not exist)
  50. action_read = FileReadAction(path='hello.sh')
  51. logger.info(action_read, extra={'msg_type': 'ACTION'})
  52. obs = runtime.run_action(action_read)
  53. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  54. assert isinstance(obs, ErrorObservation)
  55. assert 'File not found' in obs.content
  56. # Test write file
  57. action_write = FileWriteAction(content='echo "Hello, World!"', path='hello.sh')
  58. logger.info(action_write, extra={'msg_type': 'ACTION'})
  59. obs = runtime.run_action(action_write)
  60. assert isinstance(obs, FileWriteObservation)
  61. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  62. assert obs.content == ''
  63. # event stream runtime will always use absolute path
  64. assert obs.path == f'{sandbox_dir}/hello.sh'
  65. # Test read file (file should exist)
  66. action_read = FileReadAction(path='hello.sh')
  67. logger.info(action_read, extra={'msg_type': 'ACTION'})
  68. obs = runtime.run_action(action_read)
  69. assert isinstance(
  70. obs, FileReadObservation
  71. ), 'The observation should be a FileReadObservation.'
  72. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  73. assert obs.content == 'echo "Hello, World!"\n'
  74. assert obs.path == f'{sandbox_dir}/hello.sh'
  75. # clean up
  76. action = CmdRunAction(command='rm -rf hello.sh')
  77. logger.info(action, extra={'msg_type': 'ACTION'})
  78. obs = runtime.run_action(action)
  79. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  80. assert obs.exit_code == 0
  81. _close_test_runtime(runtime)
  82. @pytest.mark.skipif(
  83. TEST_IN_CI != 'True',
  84. reason='This test is not working in WSL (file ownership)',
  85. )
  86. def test_ipython_multi_user(temp_dir, runtime_cls, run_as_openhands):
  87. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  88. # Test run ipython
  89. # get username
  90. test_code = "import os; print(os.environ['USER'])"
  91. action_ipython = IPythonRunCellAction(code=test_code)
  92. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  93. obs = runtime.run_action(action_ipython)
  94. assert isinstance(obs, IPythonRunCellObservation)
  95. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  96. if run_as_openhands:
  97. assert 'openhands' in obs.content
  98. else:
  99. assert 'root' in obs.content
  100. # print the current working directory
  101. test_code = 'import os; print(os.getcwd())'
  102. action_ipython = IPythonRunCellAction(code=test_code)
  103. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  104. obs = runtime.run_action(action_ipython)
  105. assert isinstance(obs, IPythonRunCellObservation)
  106. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  107. assert (
  108. obs.content.strip()
  109. == (
  110. '/workspace\n'
  111. '[Jupyter current working directory: /workspace]\n'
  112. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  113. ).strip()
  114. )
  115. # write a file
  116. test_code = "with open('test.txt', 'w') as f: f.write('Hello, world!')"
  117. action_ipython = IPythonRunCellAction(code=test_code)
  118. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  119. obs = runtime.run_action(action_ipython)
  120. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  121. assert isinstance(obs, IPythonRunCellObservation)
  122. assert (
  123. obs.content.strip()
  124. == (
  125. '[Code executed successfully with no output]\n'
  126. '[Jupyter current working directory: /workspace]\n'
  127. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  128. ).strip()
  129. )
  130. # check file owner via bash
  131. action = CmdRunAction(command='ls -alh test.txt')
  132. logger.info(action, extra={'msg_type': 'ACTION'})
  133. obs = runtime.run_action(action)
  134. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  135. assert obs.exit_code == 0
  136. if run_as_openhands:
  137. # -rw-r--r-- 1 openhands root 13 Jul 28 03:53 test.txt
  138. assert 'openhands' in obs.content.split('\r\n')[0]
  139. else:
  140. # -rw-r--r-- 1 root root 13 Jul 28 03:53 test.txt
  141. assert 'root' in obs.content.split('\r\n')[0]
  142. # clean up
  143. action = CmdRunAction(command='rm -rf test')
  144. logger.info(action, extra={'msg_type': 'ACTION'})
  145. obs = runtime.run_action(action)
  146. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  147. assert obs.exit_code == 0
  148. _close_test_runtime(runtime)
  149. def test_ipython_simple(temp_dir, runtime_cls):
  150. runtime = _load_runtime(temp_dir, runtime_cls)
  151. sandbox_dir = _get_sandbox_folder(runtime)
  152. # Test run ipython
  153. # get username
  154. test_code = 'print(1)'
  155. action_ipython = IPythonRunCellAction(code=test_code)
  156. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  157. obs = runtime.run_action(action_ipython)
  158. assert isinstance(obs, IPythonRunCellObservation)
  159. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  160. assert (
  161. obs.content.strip()
  162. == (
  163. '1\n'
  164. f'[Jupyter current working directory: {sandbox_dir}]\n'
  165. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  166. ).strip()
  167. )
  168. _close_test_runtime(runtime)
  169. def test_ipython_package_install(temp_dir, runtime_cls, run_as_openhands):
  170. """Make sure that cd in bash also update the current working directory in ipython."""
  171. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  172. sandbox_dir = _get_sandbox_folder(runtime)
  173. # It should error out since pymsgbox is not installed
  174. action = IPythonRunCellAction(code='import pymsgbox')
  175. logger.info(action, extra={'msg_type': 'ACTION'})
  176. obs = runtime.run_action(action)
  177. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  178. assert "ModuleNotFoundError: No module named 'pymsgbox'" in obs.content
  179. # Install pymsgbox in Jupyter
  180. action = IPythonRunCellAction(code='%pip install pymsgbox==1.0.9')
  181. logger.info(action, extra={'msg_type': 'ACTION'})
  182. obs = runtime.run_action(action)
  183. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  184. assert (
  185. 'Successfully installed pymsgbox-1.0.9' in obs.content
  186. or '[Package installed successfully]' in obs.content
  187. )
  188. action = IPythonRunCellAction(code='import pymsgbox')
  189. logger.info(action, extra={'msg_type': 'ACTION'})
  190. obs = runtime.run_action(action)
  191. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  192. # import should not error out
  193. assert obs.content.strip() == (
  194. '[Code executed successfully with no output]\n'
  195. f'[Jupyter current working directory: {sandbox_dir}]\n'
  196. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  197. )
  198. _close_test_runtime(runtime)
  199. def test_ipython_file_editor_permissions_as_openhands(temp_dir, runtime_cls):
  200. """Test file editor permission behavior when running as different users."""
  201. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands=True)
  202. sandbox_dir = _get_sandbox_folder(runtime)
  203. # Create a file owned by root with restricted permissions
  204. action = CmdRunAction(
  205. command='sudo touch /root/test.txt && sudo chmod 600 /root/test.txt'
  206. )
  207. logger.info(action, extra={'msg_type': 'ACTION'})
  208. obs = runtime.run_action(action)
  209. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  210. assert obs.exit_code == 0
  211. # Try to view the file as openhands user - should fail with permission denied
  212. test_code = "print(file_editor(command='view', path='/root/test.txt'))"
  213. action = IPythonRunCellAction(code=test_code)
  214. logger.info(action, extra={'msg_type': 'ACTION'})
  215. obs = runtime.run_action(action)
  216. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  217. assert 'Permission denied' in obs.content
  218. # Try to edit the file as openhands user - should fail with permission denied
  219. test_code = "print(file_editor(command='str_replace', path='/root/test.txt', old_str='', new_str='test'))"
  220. action = IPythonRunCellAction(code=test_code)
  221. logger.info(action, extra={'msg_type': 'ACTION'})
  222. obs = runtime.run_action(action)
  223. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  224. assert 'Permission denied' in obs.content
  225. # Try to create a file in root directory - should fail with permission denied
  226. test_code = (
  227. "print(file_editor(command='create', path='/root/new.txt', file_text='test'))"
  228. )
  229. action = IPythonRunCellAction(code=test_code)
  230. logger.info(action, extra={'msg_type': 'ACTION'})
  231. obs = runtime.run_action(action)
  232. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  233. assert 'Permission denied' in obs.content
  234. # Try to use file editor in openhands sandbox directory - should work
  235. test_code = f"""
  236. # Create file
  237. print(file_editor(command='create', path='{sandbox_dir}/test.txt', file_text='Line 1\\nLine 2\\nLine 3'))
  238. # View file
  239. print(file_editor(command='view', path='{sandbox_dir}/test.txt'))
  240. # Edit file
  241. print(file_editor(command='str_replace', path='{sandbox_dir}/test.txt', old_str='Line 2', new_str='New Line 2'))
  242. # Undo edit
  243. print(file_editor(command='undo_edit', path='{sandbox_dir}/test.txt'))
  244. """
  245. action = IPythonRunCellAction(code=test_code)
  246. logger.info(action, extra={'msg_type': 'ACTION'})
  247. obs = runtime.run_action(action)
  248. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  249. assert 'File created successfully' in obs.content
  250. assert 'Line 1' in obs.content
  251. assert 'Line 2' in obs.content
  252. assert 'Line 3' in obs.content
  253. assert 'New Line 2' in obs.content
  254. assert 'Last edit to' in obs.content
  255. assert 'undone successfully' in obs.content
  256. # Clean up
  257. action = CmdRunAction(command=f'rm -f {sandbox_dir}/test.txt')
  258. logger.info(action, extra={'msg_type': 'ACTION'})
  259. obs = runtime.run_action(action)
  260. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  261. assert obs.exit_code == 0
  262. action = CmdRunAction(command='sudo rm -f /root/test.txt')
  263. logger.info(action, extra={'msg_type': 'ACTION'})
  264. obs = runtime.run_action(action)
  265. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  266. assert obs.exit_code == 0
  267. _close_test_runtime(runtime)