test_ipython.py 14 KB

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