test_bash.py 21 KB

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