test_bash.py 23 KB

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