test_ipython.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. """Test the EventStreamRuntime, which connects to the RuntimeClient 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. from openhands.runtime.client.runtime import EventStreamRuntime
  24. # ============================================================================================================================
  25. # ipython-specific tests
  26. # ============================================================================================================================
  27. def test_simple_cmd_ipython_and_fileop(temp_dir, box_class, run_as_openhands):
  28. runtime = _load_runtime(temp_dir, box_class, run_as_openhands)
  29. sandbox_dir = _get_sandbox_folder(runtime)
  30. # Test run command
  31. action_cmd = CmdRunAction(command='ls -l')
  32. logger.info(action_cmd, extra={'msg_type': 'ACTION'})
  33. obs = runtime.run_action(action_cmd)
  34. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  35. assert isinstance(obs, CmdOutputObservation)
  36. assert obs.exit_code == 0
  37. assert 'total 0' in obs.content
  38. # Test run ipython
  39. test_code = "print('Hello, `World`!\\n')"
  40. action_ipython = IPythonRunCellAction(code=test_code)
  41. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  42. obs = runtime.run_action(action_ipython)
  43. assert isinstance(obs, IPythonRunCellObservation)
  44. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  45. assert obs.content.strip() == (
  46. 'Hello, `World`!\n'
  47. f'[Jupyter current working directory: {sandbox_dir}]\n'
  48. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  49. )
  50. # Test read file (file should not exist)
  51. action_read = FileReadAction(path='hello.sh')
  52. logger.info(action_read, extra={'msg_type': 'ACTION'})
  53. obs = runtime.run_action(action_read)
  54. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  55. assert isinstance(obs, ErrorObservation)
  56. assert 'File not found' in obs.content
  57. # Test write file
  58. action_write = FileWriteAction(content='echo "Hello, World!"', path='hello.sh')
  59. logger.info(action_write, extra={'msg_type': 'ACTION'})
  60. obs = runtime.run_action(action_write)
  61. assert isinstance(obs, FileWriteObservation)
  62. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  63. assert obs.content == ''
  64. # event stream runtime will always use absolute path
  65. assert obs.path == f'{sandbox_dir}/hello.sh'
  66. # Test read file (file should exist)
  67. action_read = FileReadAction(path='hello.sh')
  68. logger.info(action_read, extra={'msg_type': 'ACTION'})
  69. obs = runtime.run_action(action_read)
  70. assert isinstance(
  71. obs, FileReadObservation
  72. ), 'The observation should be a FileReadObservation.'
  73. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  74. assert obs.content == 'echo "Hello, World!"\n'
  75. assert obs.path == f'{sandbox_dir}/hello.sh'
  76. # clean up
  77. action = CmdRunAction(command='rm -rf hello.sh')
  78. logger.info(action, extra={'msg_type': 'ACTION'})
  79. obs = runtime.run_action(action)
  80. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  81. assert obs.exit_code == 0
  82. _close_test_runtime(runtime)
  83. @pytest.mark.skipif(
  84. TEST_IN_CI != 'True',
  85. reason='This test is not working in WSL (file ownership)',
  86. )
  87. def test_ipython_multi_user(temp_dir, box_class, run_as_openhands):
  88. runtime = _load_runtime(temp_dir, box_class, run_as_openhands)
  89. # Test run ipython
  90. # get username
  91. test_code = "import os; print(os.environ['USER'])"
  92. action_ipython = IPythonRunCellAction(code=test_code)
  93. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  94. obs = runtime.run_action(action_ipython)
  95. assert isinstance(obs, IPythonRunCellObservation)
  96. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  97. if run_as_openhands:
  98. assert 'openhands' in obs.content
  99. else:
  100. assert 'root' in obs.content
  101. # print the current working directory
  102. test_code = 'import os; print(os.getcwd())'
  103. action_ipython = IPythonRunCellAction(code=test_code)
  104. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  105. obs = runtime.run_action(action_ipython)
  106. assert isinstance(obs, IPythonRunCellObservation)
  107. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  108. assert (
  109. obs.content.strip()
  110. == (
  111. '/workspace\n'
  112. '[Jupyter current working directory: /workspace]\n'
  113. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  114. ).strip()
  115. )
  116. # write a file
  117. test_code = "with open('test.txt', 'w') as f: f.write('Hello, world!')"
  118. action_ipython = IPythonRunCellAction(code=test_code)
  119. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  120. obs = runtime.run_action(action_ipython)
  121. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  122. assert isinstance(obs, IPythonRunCellObservation)
  123. assert (
  124. obs.content.strip()
  125. == (
  126. '[Code executed successfully with no output]\n'
  127. '[Jupyter current working directory: /workspace]\n'
  128. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  129. ).strip()
  130. )
  131. # check file owner via bash
  132. action = CmdRunAction(command='ls -alh test.txt')
  133. logger.info(action, extra={'msg_type': 'ACTION'})
  134. obs = runtime.run_action(action)
  135. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  136. assert obs.exit_code == 0
  137. if run_as_openhands:
  138. # -rw-r--r-- 1 openhands root 13 Jul 28 03:53 test.txt
  139. assert 'openhands' in obs.content.split('\r\n')[0]
  140. else:
  141. # -rw-r--r-- 1 root root 13 Jul 28 03:53 test.txt
  142. assert 'root' in obs.content.split('\r\n')[0]
  143. # clean up
  144. action = CmdRunAction(command='rm -rf test')
  145. logger.info(action, extra={'msg_type': 'ACTION'})
  146. obs = runtime.run_action(action)
  147. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  148. assert obs.exit_code == 0
  149. _close_test_runtime(runtime)
  150. def test_ipython_simple(temp_dir, box_class):
  151. runtime = _load_runtime(temp_dir, box_class)
  152. sandbox_dir = _get_sandbox_folder(runtime)
  153. # Test run ipython
  154. # get username
  155. test_code = 'print(1)'
  156. action_ipython = IPythonRunCellAction(code=test_code)
  157. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  158. obs = runtime.run_action(action_ipython)
  159. assert isinstance(obs, IPythonRunCellObservation)
  160. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  161. assert (
  162. obs.content.strip()
  163. == (
  164. '1\n'
  165. f'[Jupyter current working directory: {sandbox_dir}]\n'
  166. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  167. ).strip()
  168. )
  169. _close_test_runtime(runtime)
  170. def _test_ipython_agentskills_fileop_pwd_impl(
  171. runtime: EventStreamRuntime, enable_auto_lint: bool
  172. ):
  173. sandbox_dir = _get_sandbox_folder(runtime)
  174. # remove everything in /workspace
  175. action = CmdRunAction(command=f'rm -rf {sandbox_dir}/*')
  176. logger.info(action, extra={'msg_type': 'ACTION'})
  177. obs = runtime.run_action(action)
  178. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  179. assert obs.exit_code == 0
  180. action = CmdRunAction(command='mkdir test')
  181. logger.info(action, extra={'msg_type': 'ACTION'})
  182. obs = runtime.run_action(action)
  183. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  184. assert isinstance(obs, CmdOutputObservation)
  185. assert obs.exit_code == 0
  186. action = IPythonRunCellAction(code="create_file('hello.py')")
  187. logger.info(action, extra={'msg_type': 'ACTION'})
  188. obs = runtime.run_action(action)
  189. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  190. assert isinstance(obs, IPythonRunCellObservation)
  191. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  192. f'[File: {sandbox_dir}/hello.py (1 lines total)]\n'
  193. '(this is the beginning of the file)\n'
  194. '1|\n'
  195. '(this is the end of the file)\n'
  196. '[File hello.py created.]\n'
  197. f'[Jupyter current working directory: {sandbox_dir}]\n'
  198. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  199. ).strip().split('\n')
  200. action = CmdRunAction(command='cd test')
  201. logger.info(action, extra={'msg_type': 'ACTION'})
  202. obs = runtime.run_action(action)
  203. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  204. assert isinstance(obs, CmdOutputObservation)
  205. assert obs.exit_code == 0
  206. # This should create a file in the current working directory
  207. # i.e., /workspace/test/hello.py instead of /workspace/hello.py
  208. action = IPythonRunCellAction(code="create_file('hello.py')")
  209. logger.info(action, extra={'msg_type': 'ACTION'})
  210. obs = runtime.run_action(action)
  211. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  212. assert isinstance(obs, IPythonRunCellObservation)
  213. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  214. f'[File: {sandbox_dir}/test/hello.py (1 lines total)]\n'
  215. '(this is the beginning of the file)\n'
  216. '1|\n'
  217. '(this is the end of the file)\n'
  218. '[File hello.py created.]\n'
  219. f'[Jupyter current working directory: {sandbox_dir}/test]\n'
  220. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  221. ).strip().split('\n')
  222. if enable_auto_lint:
  223. # edit file, but make a mistake in indentation
  224. action = IPythonRunCellAction(
  225. code="insert_content_at_line('hello.py', 1, ' print(\"hello world\")')"
  226. )
  227. logger.info(action, extra={'msg_type': 'ACTION'})
  228. obs = runtime.run_action(action)
  229. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  230. assert isinstance(obs, IPythonRunCellObservation)
  231. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  232. f"""
  233. [Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]
  234. ERRORS:
  235. {sandbox_dir}/test/hello.py:1:3: E999 IndentationError: unexpected indent
  236. [This is how your edit would have looked if applied]
  237. -------------------------------------------------
  238. (this is the beginning of the file)
  239. 1| print("hello world")
  240. (this is the end of the file)
  241. -------------------------------------------------
  242. [This is the original code before your edit]
  243. -------------------------------------------------
  244. (this is the beginning of the file)
  245. 1|
  246. (this is the end of the file)
  247. -------------------------------------------------
  248. Your changes have NOT been applied. Please fix your edit command and try again.
  249. You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.
  250. DO NOT re-run the same failed edit command. Running it again will lead to the same error.
  251. [Jupyter current working directory: {sandbox_dir}/test]
  252. [Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
  253. """
  254. ).strip().split('\n')
  255. # edit file with correct indentation
  256. action = IPythonRunCellAction(
  257. code="insert_content_at_line('hello.py', 1, 'print(\"hello world\")')"
  258. )
  259. logger.info(action, extra={'msg_type': 'ACTION'})
  260. obs = runtime.run_action(action)
  261. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  262. assert isinstance(obs, IPythonRunCellObservation)
  263. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  264. f"""
  265. [File: {sandbox_dir}/test/hello.py (1 lines total after edit)]
  266. (this is the beginning of the file)
  267. 1|print("hello world")
  268. (this is the end of the file)
  269. [File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
  270. [Jupyter current working directory: {sandbox_dir}/test]
  271. [Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
  272. """
  273. ).strip().split('\n')
  274. action = CmdRunAction(command=f'rm -rf {sandbox_dir}/*')
  275. logger.info(action, extra={'msg_type': 'ACTION'})
  276. obs = runtime.run_action(action)
  277. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  278. assert obs.exit_code == 0
  279. def test_ipython_agentskills_fileop_pwd_with_lint(
  280. temp_dir, box_class, run_as_openhands
  281. ):
  282. runtime = _load_runtime(
  283. temp_dir, box_class, run_as_openhands, enable_auto_lint=True
  284. )
  285. _test_ipython_agentskills_fileop_pwd_impl(runtime, True)
  286. _close_test_runtime(runtime)
  287. def test_ipython_agentskills_fileop_pwd_without_lint(
  288. temp_dir, box_class, run_as_openhands
  289. ):
  290. runtime = _load_runtime(
  291. temp_dir, box_class, run_as_openhands, enable_auto_lint=False
  292. )
  293. _test_ipython_agentskills_fileop_pwd_impl(runtime, False)
  294. _close_test_runtime(runtime)
  295. def test_ipython_agentskills_fileop_pwd_with_userdir(temp_dir, box_class):
  296. """Make sure that cd in bash also update the current working directory in ipython.
  297. Handle special case where the pwd is provided as "~", which should be expanded using os.path.expanduser
  298. on the client side.
  299. """
  300. runtime = _load_runtime(
  301. temp_dir,
  302. box_class,
  303. run_as_openhands=False,
  304. )
  305. action = CmdRunAction(command='cd ~')
  306. logger.info(action, extra={'msg_type': 'ACTION'})
  307. obs = runtime.run_action(action)
  308. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  309. assert obs.exit_code == 0
  310. action = CmdRunAction(command='mkdir test && ls -la')
  311. logger.info(action, extra={'msg_type': 'ACTION'})
  312. obs = runtime.run_action(action)
  313. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  314. assert isinstance(obs, CmdOutputObservation)
  315. assert obs.exit_code == 0
  316. action = IPythonRunCellAction(code="create_file('hello.py')")
  317. logger.info(action, extra={'msg_type': 'ACTION'})
  318. obs = runtime.run_action(action)
  319. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  320. assert isinstance(obs, IPythonRunCellObservation)
  321. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  322. '[File: /root/hello.py (1 lines total)]\n'
  323. '(this is the beginning of the file)\n'
  324. '1|\n'
  325. '(this is the end of the file)\n'
  326. '[File hello.py created.]\n'
  327. '[Jupyter current working directory: /root]\n'
  328. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  329. ).strip().split('\n')
  330. action = CmdRunAction(command='cd test')
  331. logger.info(action, extra={'msg_type': 'ACTION'})
  332. obs = runtime.run_action(action)
  333. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  334. assert isinstance(obs, CmdOutputObservation)
  335. assert obs.exit_code == 0
  336. # This should create a file in the current working directory
  337. # i.e., /workspace/test/hello.py instead of /workspace/hello.py
  338. action = IPythonRunCellAction(code="create_file('hello.py')")
  339. logger.info(action, extra={'msg_type': 'ACTION'})
  340. obs = runtime.run_action(action)
  341. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  342. assert isinstance(obs, IPythonRunCellObservation)
  343. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  344. '[File: /root/test/hello.py (1 lines total)]\n'
  345. '(this is the beginning of the file)\n'
  346. '1|\n'
  347. '(this is the end of the file)\n'
  348. '[File hello.py created.]\n'
  349. '[Jupyter current working directory: /root/test]\n'
  350. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  351. ).strip().split('\n')
  352. _close_test_runtime(runtime)
  353. def test_ipython_package_install(temp_dir, box_class, run_as_openhands):
  354. """Make sure that cd in bash also update the current working directory in ipython."""
  355. runtime = _load_runtime(temp_dir, box_class, run_as_openhands)
  356. sandbox_dir = _get_sandbox_folder(runtime)
  357. # It should error out since pymsgbox is not installed
  358. action = IPythonRunCellAction(code='import pymsgbox')
  359. logger.info(action, extra={'msg_type': 'ACTION'})
  360. obs = runtime.run_action(action)
  361. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  362. assert "ModuleNotFoundError: No module named 'pymsgbox'" in obs.content
  363. # Install pymsgbox in Jupyter
  364. action = IPythonRunCellAction(code='%pip install pymsgbox==1.0.9')
  365. logger.info(action, extra={'msg_type': 'ACTION'})
  366. obs = runtime.run_action(action)
  367. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  368. assert (
  369. 'Successfully installed pymsgbox-1.0.9' in obs.content
  370. or '[Package installed successfully]' in obs.content
  371. )
  372. action = IPythonRunCellAction(code='import pymsgbox')
  373. logger.info(action, extra={'msg_type': 'ACTION'})
  374. obs = runtime.run_action(action)
  375. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  376. # import should not error out
  377. assert obs.content.strip() == (
  378. '[Code executed successfully with no output]\n'
  379. f'[Jupyter current working directory: {sandbox_dir}]\n'
  380. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  381. )
  382. _close_test_runtime(runtime)