test_bash.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. """Bash-related tests for the EventStreamRuntime, which connects to the ActionExecutor running in the sandbox."""
  2. import os
  3. from pathlib import Path
  4. import pytest
  5. from conftest import (
  6. TEST_IN_CI,
  7. _close_test_runtime,
  8. _get_sandbox_folder,
  9. _load_runtime,
  10. )
  11. from openhands.core.logger import openhands_logger as logger
  12. from openhands.events.action import CmdRunAction
  13. from openhands.events.observation import CmdOutputObservation
  14. from openhands.runtime.base import Runtime
  15. # ============================================================================================================================
  16. # Bash-specific tests
  17. # ============================================================================================================================
  18. def _run_cmd_action(runtime, custom_command: str, keep_prompt=True):
  19. action = CmdRunAction(command=custom_command, keep_prompt=keep_prompt)
  20. logger.info(action, extra={'msg_type': 'ACTION'})
  21. obs = runtime.run_action(action)
  22. assert isinstance(obs, CmdOutputObservation)
  23. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  24. return obs
  25. def test_bash_command_pexcept(temp_dir, runtime_cls, run_as_openhands):
  26. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  27. try:
  28. # We set env var PS1="\u@\h:\w $"
  29. # and construct the PEXCEPT prompt base on it.
  30. # When run `env`, bad implementation of CmdRunAction will be pexcepted by this
  31. # and failed to pexcept the right content, causing it fail to get error code.
  32. obs = runtime.run_action(CmdRunAction(command='env'))
  33. # For example:
  34. # 02:16:13 - openhands:DEBUG: client.py:78 - Executing command: env
  35. # 02:16:13 - openhands:DEBUG: client.py:82 - Command output: PYTHONUNBUFFERED=1
  36. # CONDA_EXE=/openhands/miniforge3/bin/conda
  37. # [...]
  38. # LC_CTYPE=C.UTF-8
  39. # PS1=\u@\h:\w $
  40. # 02:16:13 - openhands:DEBUG: client.py:89 - Executing command for exit code: env
  41. # 02:16:13 - openhands:DEBUG: client.py:92 - Exit code Output:
  42. # CONDA_DEFAULT_ENV=base
  43. # As long as the exit code is 0, the test will pass.
  44. assert isinstance(
  45. obs, CmdOutputObservation
  46. ), 'The observation should be a CmdOutputObservation.'
  47. assert obs.exit_code == 0, 'The exit code should be 0.'
  48. finally:
  49. _close_test_runtime(runtime)
  50. def test_bash_timeout_and_keyboard_interrupt(temp_dir, runtime_cls, run_as_openhands):
  51. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  52. try:
  53. action = CmdRunAction(command='python -c "import time; time.sleep(10)"')
  54. action.timeout = 1
  55. obs = runtime.run_action(action)
  56. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  57. assert isinstance(obs, CmdOutputObservation)
  58. assert (
  59. '[Command timed out after 1 seconds. SIGINT was sent to interrupt the command.]'
  60. in obs.content
  61. )
  62. assert 'KeyboardInterrupt' in obs.content
  63. # follow up command should not be affected
  64. action = CmdRunAction(command='ls')
  65. action.timeout = 1
  66. obs = runtime.run_action(action)
  67. assert isinstance(obs, CmdOutputObservation)
  68. assert obs.exit_code == 0
  69. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  70. # run it again!
  71. action = CmdRunAction(command='python -c "import time; time.sleep(10)"')
  72. action.timeout = 1
  73. obs = runtime.run_action(action)
  74. assert isinstance(obs, CmdOutputObservation)
  75. assert (
  76. '[Command timed out after 1 seconds. SIGINT was sent to interrupt the command.]'
  77. in obs.content
  78. )
  79. assert 'KeyboardInterrupt' in obs.content
  80. # things should still work
  81. action = CmdRunAction(command='ls')
  82. action.timeout = 1
  83. obs = runtime.run_action(action)
  84. assert isinstance(obs, CmdOutputObservation)
  85. assert obs.exit_code == 0
  86. assert '/workspace' in obs.interpreter_details
  87. finally:
  88. _close_test_runtime(runtime)
  89. def test_bash_pexcept_eof(temp_dir, runtime_cls, run_as_openhands):
  90. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  91. try:
  92. action = CmdRunAction(command='python3 -m http.server 8080')
  93. action.timeout = 1
  94. obs = runtime.run_action(action)
  95. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  96. assert isinstance(obs, CmdOutputObservation)
  97. assert obs.exit_code == 130 # script was killed by SIGINT
  98. assert 'Serving HTTP on 0.0.0.0 port 8080' in obs.content
  99. assert 'Keyboard interrupt received, exiting.' in obs.content
  100. action = CmdRunAction(command='ls')
  101. action.timeout = 1
  102. obs = runtime.run_action(action)
  103. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  104. assert isinstance(obs, CmdOutputObservation)
  105. assert obs.exit_code == 0
  106. assert '/workspace' in obs.interpreter_details
  107. # run it again!
  108. action = CmdRunAction(command='python3 -m http.server 8080')
  109. action.timeout = 1
  110. obs = runtime.run_action(action)
  111. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  112. assert isinstance(obs, CmdOutputObservation)
  113. assert obs.exit_code == 130 # script was killed by SIGINT
  114. assert 'Serving HTTP on 0.0.0.0 port 8080' in obs.content
  115. assert 'Keyboard interrupt received, exiting.' in obs.content
  116. # things should still work
  117. action = CmdRunAction(command='ls')
  118. action.timeout = 1
  119. obs = runtime.run_action(action)
  120. assert isinstance(obs, CmdOutputObservation)
  121. assert obs.exit_code == 0
  122. assert '/workspace' in obs.interpreter_details
  123. finally:
  124. _close_test_runtime(runtime)
  125. def test_process_resistant_to_one_sigint(temp_dir, runtime_cls, run_as_openhands):
  126. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  127. try:
  128. # Create a bash script that ignores SIGINT up to 1 times
  129. script_content = """
  130. #!/bin/bash
  131. trap_count=0
  132. trap 'echo "Caught SIGINT ($((++trap_count))/1), ignoring..."; [ $trap_count -ge 1 ] && trap - INT && exit' INT
  133. while true; do
  134. echo "Still running..."
  135. sleep 1
  136. done
  137. """.strip()
  138. with open(f'{temp_dir}/resistant_script.sh', 'w') as f:
  139. f.write(script_content)
  140. os.chmod(f'{temp_dir}/resistant_script.sh', 0o777)
  141. runtime.copy_to(
  142. os.path.join(temp_dir, 'resistant_script.sh'),
  143. runtime.config.workspace_mount_path_in_sandbox,
  144. )
  145. # Run the resistant script
  146. action = CmdRunAction(command='sudo bash ./resistant_script.sh')
  147. action.timeout = 5
  148. action.blocking = True
  149. logger.info(action, extra={'msg_type': 'ACTION'})
  150. obs = runtime.run_action(action)
  151. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  152. assert isinstance(obs, CmdOutputObservation)
  153. assert obs.exit_code == 130 # script was killed by SIGINT
  154. assert 'Still running...' in obs.content
  155. assert 'Caught SIGINT (1/1), ignoring...' in obs.content
  156. assert 'Stopped' not in obs.content
  157. assert (
  158. '[Command timed out after 5 seconds. SIGINT was sent to interrupt the command.]'
  159. in obs.content
  160. )
  161. # Normal command should still work
  162. action = CmdRunAction(command='ls')
  163. action.timeout = 10
  164. obs = runtime.run_action(action)
  165. assert isinstance(obs, CmdOutputObservation)
  166. assert obs.exit_code == 0
  167. assert '/workspace' in obs.interpreter_details
  168. assert 'resistant_script.sh' in obs.content
  169. finally:
  170. _close_test_runtime(runtime)
  171. def test_process_resistant_to_multiple_sigint(temp_dir, runtime_cls, run_as_openhands):
  172. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  173. try:
  174. # Create a bash script that ignores SIGINT up to 2 times
  175. script_content = """
  176. #!/bin/bash
  177. trap_count=0
  178. trap 'echo "Caught SIGINT ($((++trap_count))/3), ignoring..."; [ $trap_count -ge 3 ] && trap - INT && exit' INT
  179. while true; do
  180. echo "Still running..."
  181. sleep 1
  182. done
  183. """.strip()
  184. with open(f'{temp_dir}/resistant_script.sh', 'w') as f:
  185. f.write(script_content)
  186. os.chmod(f'{temp_dir}/resistant_script.sh', 0o777)
  187. runtime.copy_to(
  188. os.path.join(temp_dir, 'resistant_script.sh'),
  189. runtime.config.workspace_mount_path_in_sandbox,
  190. )
  191. # Run the resistant script
  192. action = CmdRunAction(command='sudo bash ./resistant_script.sh')
  193. action.timeout = 2
  194. action.blocking = True
  195. logger.info(action, extra={'msg_type': 'ACTION'})
  196. obs = runtime.run_action(action)
  197. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  198. assert isinstance(obs, CmdOutputObservation)
  199. assert obs.exit_code == 0
  200. assert 'Still running...' in obs.content
  201. assert 'Caught SIGINT (1/3), ignoring...' in obs.content
  202. assert '[1]+' and 'Stopped' in obs.content
  203. assert (
  204. '[Command timed out after 2 seconds. SIGINT was sent to interrupt the command, but failed. The command was killed.]'
  205. in obs.content
  206. )
  207. # Normal command should still work
  208. action = CmdRunAction(command='ls')
  209. action.timeout = 10
  210. obs = runtime.run_action(action)
  211. assert isinstance(obs, CmdOutputObservation)
  212. assert obs.exit_code == 0
  213. assert '/workspace' in obs.interpreter_details
  214. assert 'resistant_script.sh' in obs.content
  215. finally:
  216. _close_test_runtime(runtime)
  217. def test_multiline_commands(temp_dir, runtime_cls):
  218. runtime = _load_runtime(temp_dir, runtime_cls)
  219. try:
  220. # single multiline command
  221. obs = _run_cmd_action(runtime, 'echo \\\n -e "foo"')
  222. assert obs.exit_code == 0, 'The exit code should be 0.'
  223. assert 'foo' in obs.content
  224. # test multiline echo
  225. obs = _run_cmd_action(runtime, 'echo -e "hello\nworld"')
  226. assert obs.exit_code == 0, 'The exit code should be 0.'
  227. assert 'hello\r\nworld' in obs.content
  228. # test whitespace
  229. obs = _run_cmd_action(runtime, 'echo -e "a\\n\\n\\nz"')
  230. assert obs.exit_code == 0, 'The exit code should be 0.'
  231. assert '\r\n\r\n\r\n' in obs.content
  232. finally:
  233. _close_test_runtime(runtime)
  234. def test_multiple_multiline_commands(temp_dir, runtime_cls, run_as_openhands):
  235. cmds = [
  236. 'ls -l',
  237. 'echo -e "hello\nworld"',
  238. """
  239. echo -e "hello it\\'s me"
  240. """.strip(),
  241. """
  242. echo \\
  243. -e 'hello' \\
  244. -v
  245. """.strip(),
  246. """
  247. echo -e 'hello\\nworld\\nare\\nyou\\nthere?'
  248. """.strip(),
  249. """
  250. echo -e 'hello
  251. world
  252. are
  253. you\\n
  254. there?'
  255. """.strip(),
  256. """
  257. echo -e 'hello
  258. world "
  259. '
  260. """.strip(),
  261. ]
  262. joined_cmds = '\n'.join(cmds)
  263. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  264. try:
  265. obs = _run_cmd_action(runtime, joined_cmds)
  266. assert obs.exit_code == 0, 'The exit code should be 0.'
  267. assert 'total 0' in obs.content
  268. assert 'hello\r\nworld' in obs.content
  269. assert "hello it\\'s me" in obs.content
  270. assert 'hello -v' in obs.content
  271. assert 'hello\r\nworld\r\nare\r\nyou\r\nthere?' in obs.content
  272. assert 'hello\r\nworld\r\nare\r\nyou\r\n\r\nthere?' in obs.content
  273. finally:
  274. _close_test_runtime(runtime)
  275. def test_no_ps2_in_output(temp_dir, runtime_cls, run_as_openhands):
  276. """Test that the PS2 sign is not added to the output of a multiline command."""
  277. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  278. try:
  279. obs = _run_cmd_action(runtime, 'echo -e "hello\nworld"')
  280. assert obs.exit_code == 0, 'The exit code should be 0.'
  281. assert 'hello\r\nworld' in obs.content
  282. assert '>' not in obs.content
  283. finally:
  284. _close_test_runtime(runtime)
  285. def test_multiline_command_loop(temp_dir, runtime_cls):
  286. # https://github.com/All-Hands-AI/OpenHands/issues/3143
  287. init_cmd = """
  288. mkdir -p _modules && \
  289. for month in {01..04}; do
  290. for day in {01..05}; do
  291. touch "_modules/2024-${month}-${day}-sample.md"
  292. done
  293. done
  294. echo "created files"
  295. """
  296. follow_up_cmd = """
  297. for file in _modules/*.md; do
  298. new_date=$(echo $file | sed -E 's/2024-(01|02|03|04)-/2024-/;s/2024-01/2024-08/;s/2024-02/2024-09/;s/2024-03/2024-10/;s/2024-04/2024-11/')
  299. mv "$file" "$new_date"
  300. done
  301. echo "success"
  302. """
  303. runtime = _load_runtime(temp_dir, runtime_cls)
  304. try:
  305. obs = _run_cmd_action(runtime, init_cmd)
  306. assert obs.exit_code == 0, 'The exit code should be 0.'
  307. assert 'created files' in obs.content
  308. obs = _run_cmd_action(runtime, follow_up_cmd)
  309. assert obs.exit_code == 0, 'The exit code should be 0.'
  310. assert 'success' in obs.content
  311. finally:
  312. _close_test_runtime(runtime)
  313. def test_cmd_run(temp_dir, runtime_cls, run_as_openhands):
  314. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  315. try:
  316. obs = _run_cmd_action(runtime, 'ls -l /openhands/workspace')
  317. assert obs.exit_code == 0
  318. obs = _run_cmd_action(runtime, 'ls -l')
  319. assert obs.exit_code == 0
  320. assert 'total 0' in obs.content
  321. obs = _run_cmd_action(runtime, 'mkdir test')
  322. assert obs.exit_code == 0
  323. obs = _run_cmd_action(runtime, 'ls -l')
  324. assert obs.exit_code == 0
  325. if run_as_openhands:
  326. assert 'openhands' in obs.content
  327. else:
  328. assert 'root' in obs.content
  329. assert 'test' in obs.content
  330. obs = _run_cmd_action(runtime, 'touch test/foo.txt')
  331. assert obs.exit_code == 0
  332. obs = _run_cmd_action(runtime, 'ls -l test')
  333. assert obs.exit_code == 0
  334. assert 'foo.txt' in obs.content
  335. # clean up: this is needed, since CI will not be
  336. # run as root, and this test may leave a file
  337. # owned by root
  338. _run_cmd_action(runtime, 'rm -rf test')
  339. assert obs.exit_code == 0
  340. finally:
  341. _close_test_runtime(runtime)
  342. def test_run_as_user_correct_home_dir(temp_dir, runtime_cls, run_as_openhands):
  343. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  344. try:
  345. obs = _run_cmd_action(runtime, 'cd ~ && pwd')
  346. assert obs.exit_code == 0
  347. if run_as_openhands:
  348. assert '/home/openhands' in obs.content
  349. else:
  350. assert '/root' in obs.content
  351. finally:
  352. _close_test_runtime(runtime)
  353. def test_multi_cmd_run_in_single_line(temp_dir, runtime_cls):
  354. runtime = _load_runtime(temp_dir, runtime_cls)
  355. try:
  356. obs = _run_cmd_action(runtime, 'pwd && ls -l')
  357. assert obs.exit_code == 0
  358. assert '/workspace' in obs.content
  359. assert 'total 0' in obs.content
  360. finally:
  361. _close_test_runtime(runtime)
  362. def test_stateful_cmd(temp_dir, runtime_cls):
  363. runtime = _load_runtime(temp_dir, runtime_cls)
  364. sandbox_dir = _get_sandbox_folder(runtime)
  365. try:
  366. obs = _run_cmd_action(runtime, 'mkdir -p test')
  367. assert obs.exit_code == 0, 'The exit code should be 0.'
  368. obs = _run_cmd_action(runtime, 'cd test')
  369. assert obs.exit_code == 0, 'The exit code should be 0.'
  370. obs = _run_cmd_action(runtime, 'pwd')
  371. assert obs.exit_code == 0, 'The exit code should be 0.'
  372. assert f'{sandbox_dir}/test' in obs.content
  373. finally:
  374. _close_test_runtime(runtime)
  375. def test_failed_cmd(temp_dir, runtime_cls):
  376. runtime = _load_runtime(temp_dir, runtime_cls)
  377. try:
  378. obs = _run_cmd_action(runtime, 'non_existing_command')
  379. assert obs.exit_code != 0, 'The exit code should not be 0 for a failed command.'
  380. finally:
  381. _close_test_runtime(runtime)
  382. def _create_test_file(host_temp_dir):
  383. # Single file
  384. with open(os.path.join(host_temp_dir, 'test_file.txt'), 'w') as f:
  385. f.write('Hello, World!')
  386. def test_copy_single_file(temp_dir, runtime_cls):
  387. runtime = _load_runtime(temp_dir, runtime_cls)
  388. try:
  389. sandbox_dir = _get_sandbox_folder(runtime)
  390. sandbox_file = os.path.join(sandbox_dir, 'test_file.txt')
  391. _create_test_file(temp_dir)
  392. runtime.copy_to(os.path.join(temp_dir, 'test_file.txt'), sandbox_dir)
  393. obs = _run_cmd_action(runtime, f'ls -alh {sandbox_dir}')
  394. assert obs.exit_code == 0
  395. assert 'test_file.txt' in obs.content
  396. obs = _run_cmd_action(runtime, f'cat {sandbox_file}')
  397. assert obs.exit_code == 0
  398. assert 'Hello, World!' in obs.content
  399. finally:
  400. _close_test_runtime(runtime)
  401. def _create_host_test_dir_with_files(test_dir):
  402. logger.debug(f'creating `{test_dir}`')
  403. if not os.path.isdir(test_dir):
  404. os.makedirs(test_dir, exist_ok=True)
  405. logger.debug('creating test files in `test_dir`')
  406. with open(os.path.join(test_dir, 'file1.txt'), 'w') as f:
  407. f.write('File 1 content')
  408. with open(os.path.join(test_dir, 'file2.txt'), 'w') as f:
  409. f.write('File 2 content')
  410. def test_copy_directory_recursively(temp_dir, runtime_cls):
  411. runtime = _load_runtime(temp_dir, runtime_cls)
  412. sandbox_dir = _get_sandbox_folder(runtime)
  413. try:
  414. temp_dir_copy = os.path.join(temp_dir, 'test_dir')
  415. # We need a separate directory, since temp_dir is mounted to /workspace
  416. _create_host_test_dir_with_files(temp_dir_copy)
  417. runtime.copy_to(temp_dir_copy, sandbox_dir, recursive=True)
  418. obs = _run_cmd_action(runtime, f'ls -alh {sandbox_dir}')
  419. assert obs.exit_code == 0
  420. assert 'test_dir' in obs.content
  421. assert 'file1.txt' not in obs.content
  422. assert 'file2.txt' not in obs.content
  423. obs = _run_cmd_action(runtime, f'ls -alh {sandbox_dir}/test_dir')
  424. assert obs.exit_code == 0
  425. assert 'file1.txt' in obs.content
  426. assert 'file2.txt' in obs.content
  427. obs = _run_cmd_action(runtime, f'cat {sandbox_dir}/test_dir/file1.txt')
  428. assert obs.exit_code == 0
  429. assert 'File 1 content' in obs.content
  430. finally:
  431. _close_test_runtime(runtime)
  432. def test_copy_to_non_existent_directory(temp_dir, runtime_cls):
  433. runtime = _load_runtime(temp_dir, runtime_cls)
  434. try:
  435. sandbox_dir = _get_sandbox_folder(runtime)
  436. _create_test_file(temp_dir)
  437. runtime.copy_to(
  438. os.path.join(temp_dir, 'test_file.txt'), f'{sandbox_dir}/new_dir'
  439. )
  440. obs = _run_cmd_action(runtime, f'cat {sandbox_dir}/new_dir/test_file.txt')
  441. assert obs.exit_code == 0
  442. assert 'Hello, World!' in obs.content
  443. finally:
  444. _close_test_runtime(runtime)
  445. def test_overwrite_existing_file(temp_dir, runtime_cls):
  446. runtime = _load_runtime(temp_dir, runtime_cls)
  447. try:
  448. sandbox_dir = _get_sandbox_folder(runtime)
  449. obs = _run_cmd_action(runtime, f'ls -alh {sandbox_dir}')
  450. assert obs.exit_code == 0
  451. obs = _run_cmd_action(runtime, f'touch {sandbox_dir}/test_file.txt')
  452. assert obs.exit_code == 0
  453. obs = _run_cmd_action(runtime, f'ls -alh {sandbox_dir}')
  454. assert obs.exit_code == 0
  455. obs = _run_cmd_action(runtime, f'cat {sandbox_dir}/test_file.txt')
  456. assert obs.exit_code == 0
  457. assert 'Hello, World!' not in obs.content
  458. _create_test_file(temp_dir)
  459. runtime.copy_to(os.path.join(temp_dir, 'test_file.txt'), sandbox_dir)
  460. obs = _run_cmd_action(runtime, f'cat {sandbox_dir}/test_file.txt')
  461. assert obs.exit_code == 0
  462. assert 'Hello, World!' in obs.content
  463. finally:
  464. _close_test_runtime(runtime)
  465. def test_copy_non_existent_file(temp_dir, runtime_cls):
  466. runtime = _load_runtime(temp_dir, runtime_cls)
  467. try:
  468. sandbox_dir = _get_sandbox_folder(runtime)
  469. with pytest.raises(FileNotFoundError):
  470. runtime.copy_to(
  471. os.path.join(sandbox_dir, 'non_existent_file.txt'),
  472. f'{sandbox_dir}/should_not_exist.txt',
  473. )
  474. obs = _run_cmd_action(runtime, f'ls {sandbox_dir}/should_not_exist.txt')
  475. assert obs.exit_code != 0 # File should not exist
  476. finally:
  477. _close_test_runtime(runtime)
  478. def test_copy_from_directory(temp_dir, runtime_cls):
  479. runtime: Runtime = _load_runtime(temp_dir, runtime_cls)
  480. sandbox_dir = _get_sandbox_folder(runtime)
  481. try:
  482. temp_dir_copy = os.path.join(temp_dir, 'test_dir')
  483. # We need a separate directory, since temp_dir is mounted to /workspace
  484. _create_host_test_dir_with_files(temp_dir_copy)
  485. # Initial state
  486. runtime.copy_to(temp_dir_copy, sandbox_dir, recursive=True)
  487. path_to_copy_from = f'{sandbox_dir}/test_dir'
  488. result = runtime.copy_from(path=path_to_copy_from)
  489. # Result is returned as a path
  490. assert isinstance(result, Path)
  491. result.unlink()
  492. finally:
  493. _close_test_runtime(runtime)
  494. def test_keep_prompt(runtime_cls, temp_dir):
  495. runtime = _load_runtime(
  496. temp_dir,
  497. runtime_cls=runtime_cls,
  498. run_as_openhands=False,
  499. )
  500. try:
  501. sandbox_dir = _get_sandbox_folder(runtime)
  502. obs = _run_cmd_action(runtime, f'touch {sandbox_dir}/test_file.txt')
  503. assert obs.exit_code == 0
  504. assert 'root@' in obs.interpreter_details
  505. obs = _run_cmd_action(
  506. runtime, f'cat {sandbox_dir}/test_file.txt', keep_prompt=False
  507. )
  508. assert obs.exit_code == 0
  509. assert 'root@' not in obs.interpreter_details
  510. finally:
  511. _close_test_runtime(runtime)
  512. @pytest.mark.skipif(
  513. TEST_IN_CI != 'True',
  514. reason='This test is not working in WSL (file ownership)',
  515. )
  516. def test_git_operation(runtime_cls):
  517. # do not mount workspace, since workspace mount by tests will be owned by root
  518. # while the user_id we get via os.getuid() is different from root
  519. # which causes permission issues
  520. runtime = _load_runtime(
  521. temp_dir=None,
  522. runtime_cls=runtime_cls,
  523. # Need to use non-root user to expose issues
  524. run_as_openhands=True,
  525. )
  526. # this will happen if permission of runtime is not properly configured
  527. # fatal: detected dubious ownership in repository at '/workspace'
  528. try:
  529. # check the ownership of the current directory
  530. obs = _run_cmd_action(runtime, 'ls -alh .')
  531. assert obs.exit_code == 0
  532. # drwx--S--- 2 openhands root 64 Aug 7 23:32 .
  533. # drwxr-xr-x 1 root root 4.0K Aug 7 23:33 ..
  534. for line in obs.content.split('\r\n'):
  535. if ' ..' in line:
  536. # parent directory should be owned by root
  537. assert 'root' in line
  538. assert 'openhands' not in line
  539. elif ' .' in line:
  540. # current directory should be owned by openhands
  541. # and its group should be root
  542. assert 'openhands' in line
  543. assert 'root' in line
  544. # make sure all git operations are allowed
  545. obs = _run_cmd_action(runtime, 'git init')
  546. assert obs.exit_code == 0
  547. # create a file
  548. obs = _run_cmd_action(runtime, 'echo "hello" > test_file.txt')
  549. assert obs.exit_code == 0
  550. # git add
  551. obs = _run_cmd_action(runtime, 'git add test_file.txt')
  552. assert obs.exit_code == 0
  553. # git diff
  554. obs = _run_cmd_action(runtime, 'git diff')
  555. assert obs.exit_code == 0
  556. # git commit
  557. obs = _run_cmd_action(runtime, 'git commit -m "test commit"')
  558. assert obs.exit_code == 0
  559. finally:
  560. _close_test_runtime(runtime)
  561. def test_python_version(temp_dir, runtime_cls, run_as_openhands):
  562. runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
  563. try:
  564. obs = runtime.run_action(CmdRunAction(command='python --version'))
  565. assert isinstance(
  566. obs, CmdOutputObservation
  567. ), 'The observation should be a CmdOutputObservation.'
  568. assert obs.exit_code == 0, 'The exit code should be 0.'
  569. assert 'Python 3' in obs.content, 'The output should contain "Python 3".'
  570. finally:
  571. _close_test_runtime(runtime)