test_is_stuck.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import logging
  2. import random
  3. from unittest.mock import Mock, patch
  4. import pytest
  5. from pytest import TempPathFactory
  6. from openhands.controller.agent_controller import AgentController
  7. from openhands.controller.state.state import State
  8. from openhands.controller.stuck import StuckDetector
  9. from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
  10. from openhands.events.action.commands import IPythonRunCellAction
  11. from openhands.events.observation import (
  12. CmdOutputObservation,
  13. FileReadObservation,
  14. )
  15. from openhands.events.observation.commands import IPythonRunCellObservation
  16. from openhands.events.observation.empty import NullObservation
  17. from openhands.events.observation.error import ErrorObservation
  18. from openhands.events.stream import EventSource, EventStream
  19. from openhands.memory.history import ShortTermHistory
  20. from openhands.storage import get_file_store
  21. def collect_events(stream):
  22. return [event for event in stream.get_events()]
  23. logging.basicConfig(level=logging.DEBUG)
  24. jupyter_line_1 = '\n[Jupyter current working directory:'
  25. jupyter_line_2 = '\n[Jupyter Python interpreter:'
  26. @pytest.fixture
  27. def temp_dir(tmp_path_factory: TempPathFactory) -> str:
  28. return str(tmp_path_factory.mktemp('test_is_stuck'))
  29. @pytest.fixture
  30. def event_stream(temp_dir):
  31. file_store = get_file_store('local', temp_dir)
  32. event_stream = EventStream('asdf', file_store)
  33. yield event_stream
  34. # clear after each test
  35. event_stream.clear()
  36. class TestStuckDetector:
  37. @pytest.fixture
  38. def stuck_detector(self, event_stream):
  39. state = State(inputs={}, max_iterations=50)
  40. state.history.set_event_stream(event_stream)
  41. return StuckDetector(state)
  42. def _impl_syntax_error_events(
  43. self, event_stream: EventStream, error_message: str, random_line: bool
  44. ):
  45. for _ in range(4):
  46. ipython_action = IPythonRunCellAction(code='print("hello')
  47. event_stream.add_event(ipython_action, EventSource.AGENT)
  48. extra_number = random.randint(88, 222) if random_line else '42'
  49. extra_line = '\n' * random.randint(2, 3) if random_line else ''
  50. ipython_observation = IPythonRunCellObservation(
  51. content=f' Cell In[1], line {extra_number}\n'
  52. 'to_replace="""def largest(min_factor, max_factor):\n ^\n'
  53. f'{error_message}{extra_line}' + jupyter_line_1 + jupyter_line_2,
  54. code='print("hello',
  55. )
  56. print(ipython_observation.content)
  57. ipython_observation._cause = ipython_action._id
  58. event_stream.add_event(ipython_observation, EventSource.USER)
  59. def _impl_unterminated_string_error_events(
  60. self, event_stream: EventStream, random_line: bool
  61. ):
  62. for i in range(4):
  63. ipython_action = IPythonRunCellAction(code='print("hello')
  64. event_stream.add_event(ipython_action, EventSource.AGENT)
  65. line_number = i * 10 if random_line else '1'
  66. ipython_observation = IPythonRunCellObservation(
  67. content=f'print(" Cell In[1], line {line_number}\nhello\n ^\nSyntaxError: unterminated string literal (detected at line {line_number})'
  68. + jupyter_line_1
  69. + jupyter_line_2,
  70. code='print("hello',
  71. )
  72. ipython_observation._cause = ipython_action._id
  73. event_stream.add_event(ipython_observation, EventSource.USER)
  74. def test_history_too_short(
  75. self, stuck_detector: StuckDetector, event_stream: EventStream
  76. ):
  77. message_action = MessageAction(content='Hello', wait_for_response=False)
  78. message_action._source = EventSource.USER
  79. observation = NullObservation(content='')
  80. observation._cause = message_action.id
  81. event_stream.add_event(message_action, EventSource.USER)
  82. event_stream.add_event(observation, EventSource.USER)
  83. cmd_action = CmdRunAction(command='ls')
  84. event_stream.add_event(cmd_action, EventSource.AGENT)
  85. cmd_observation = CmdOutputObservation(
  86. command_id=1, command='ls', content='file1.txt\nfile2.txt'
  87. )
  88. cmd_observation._cause = cmd_action._id
  89. event_stream.add_event(cmd_observation, EventSource.USER)
  90. # stuck_detector.state.history.set_event_stream(event_stream)
  91. assert stuck_detector.is_stuck() is False
  92. def test_is_stuck_repeating_action_observation(
  93. self, stuck_detector: StuckDetector, event_stream: EventStream
  94. ):
  95. message_action = MessageAction(content='Done', wait_for_response=False)
  96. message_action._source = EventSource.USER
  97. hello_action = MessageAction(content='Hello', wait_for_response=False)
  98. hello_observation = NullObservation('')
  99. # 2 events
  100. event_stream.add_event(hello_action, EventSource.USER)
  101. event_stream.add_event(hello_observation, EventSource.USER)
  102. cmd_action_1 = CmdRunAction(command='ls')
  103. event_stream.add_event(cmd_action_1, EventSource.AGENT)
  104. cmd_observation_1 = CmdOutputObservation(
  105. content='', command='ls', command_id=cmd_action_1._id
  106. )
  107. cmd_observation_1._cause = cmd_action_1._id
  108. event_stream.add_event(cmd_observation_1, EventSource.USER)
  109. # 4 events
  110. cmd_action_2 = CmdRunAction(command='ls')
  111. event_stream.add_event(cmd_action_2, EventSource.AGENT)
  112. cmd_observation_2 = CmdOutputObservation(
  113. content='', command='ls', command_id=cmd_action_2._id
  114. )
  115. cmd_observation_2._cause = cmd_action_2._id
  116. event_stream.add_event(cmd_observation_2, EventSource.USER)
  117. # 6 events
  118. # random user message just because we can
  119. message_null_observation = NullObservation(content='')
  120. event_stream.add_event(message_action, EventSource.USER)
  121. event_stream.add_event(message_null_observation, EventSource.USER)
  122. # 8 events
  123. assert stuck_detector.is_stuck() is False
  124. assert stuck_detector.state.almost_stuck == 2
  125. cmd_action_3 = CmdRunAction(command='ls')
  126. event_stream.add_event(cmd_action_3, EventSource.AGENT)
  127. cmd_observation_3 = CmdOutputObservation(
  128. content='', command='ls', command_id=cmd_action_3._id
  129. )
  130. cmd_observation_3._cause = cmd_action_3._id
  131. event_stream.add_event(cmd_observation_3, EventSource.USER)
  132. # 10 events
  133. assert len(collect_events(event_stream)) == 10
  134. assert len(list(stuck_detector.state.history.get_events())) == 8
  135. assert len(stuck_detector.state.history.get_pairs()) == 5
  136. assert stuck_detector.is_stuck() is False
  137. assert stuck_detector.state.almost_stuck == 1
  138. cmd_action_4 = CmdRunAction(command='ls')
  139. event_stream.add_event(cmd_action_4, EventSource.AGENT)
  140. cmd_observation_4 = CmdOutputObservation(
  141. content='', command='ls', command_id=cmd_action_4._id
  142. )
  143. cmd_observation_4._cause = cmd_action_4._id
  144. event_stream.add_event(cmd_observation_4, EventSource.USER)
  145. # 12 events
  146. assert len(collect_events(event_stream)) == 12
  147. assert len(list(stuck_detector.state.history.get_events())) == 10
  148. assert len(stuck_detector.state.history.get_pairs()) == 6
  149. with patch('logging.Logger.warning') as mock_warning:
  150. assert stuck_detector.is_stuck() is True
  151. assert stuck_detector.state.almost_stuck == 0
  152. mock_warning.assert_called_once_with('Action, Observation loop detected')
  153. def test_is_stuck_repeating_action_error(
  154. self, stuck_detector: StuckDetector, event_stream: EventStream
  155. ):
  156. # (action, error_observation), not necessarily the same error
  157. message_action = MessageAction(content='Done', wait_for_response=False)
  158. message_action._source = EventSource.USER
  159. hello_action = MessageAction(content='Hello', wait_for_response=False)
  160. hello_observation = NullObservation(content='')
  161. event_stream.add_event(hello_action, EventSource.USER)
  162. hello_observation._cause = hello_action._id
  163. event_stream.add_event(hello_observation, EventSource.USER)
  164. # 2 events
  165. cmd_action_1 = CmdRunAction(command='invalid_command')
  166. event_stream.add_event(cmd_action_1, EventSource.AGENT)
  167. error_observation_1 = ErrorObservation(content='Command not found')
  168. error_observation_1._cause = cmd_action_1._id
  169. event_stream.add_event(error_observation_1, EventSource.USER)
  170. # 4 events
  171. cmd_action_2 = CmdRunAction(command='invalid_command')
  172. event_stream.add_event(cmd_action_2, EventSource.AGENT)
  173. error_observation_2 = ErrorObservation(
  174. content='Command still not found or another error'
  175. )
  176. error_observation_2._cause = cmd_action_2._id
  177. event_stream.add_event(error_observation_2, EventSource.USER)
  178. # 6 events
  179. message_null_observation = NullObservation(content='')
  180. event_stream.add_event(message_action, EventSource.USER)
  181. event_stream.add_event(message_null_observation, EventSource.USER)
  182. # 8 events
  183. cmd_action_3 = CmdRunAction(command='invalid_command')
  184. event_stream.add_event(cmd_action_3, EventSource.AGENT)
  185. error_observation_3 = ErrorObservation(content='Different error')
  186. error_observation_3._cause = cmd_action_3._id
  187. event_stream.add_event(error_observation_3, EventSource.USER)
  188. # 10 events
  189. cmd_action_4 = CmdRunAction(command='invalid_command')
  190. event_stream.add_event(cmd_action_4, EventSource.AGENT)
  191. error_observation_4 = ErrorObservation(content='Command not found')
  192. error_observation_4._cause = cmd_action_4._id
  193. event_stream.add_event(error_observation_4, EventSource.USER)
  194. # 12 events
  195. with patch('logging.Logger.warning') as mock_warning:
  196. assert stuck_detector.is_stuck() is True
  197. mock_warning.assert_called_once_with(
  198. 'Action, ErrorObservation loop detected'
  199. )
  200. def test_is_stuck_invalid_syntax_error(
  201. self, stuck_detector: StuckDetector, event_stream: EventStream
  202. ):
  203. self._impl_syntax_error_events(
  204. event_stream,
  205. error_message='SyntaxError: invalid syntax. Perhaps you forgot a comma?',
  206. random_line=False,
  207. )
  208. with patch('logging.Logger.warning'):
  209. assert stuck_detector.is_stuck() is True
  210. def test_is_not_stuck_invalid_syntax_error(
  211. self, stuck_detector: StuckDetector, event_stream: EventStream
  212. ):
  213. self._impl_syntax_error_events(
  214. event_stream,
  215. error_message='SyntaxError: invalid syntax. Perhaps you forgot a comma?',
  216. random_line=True,
  217. )
  218. with patch('logging.Logger.warning'):
  219. assert stuck_detector.is_stuck() is False
  220. def test_is_stuck_incomplete_input_error(
  221. self, stuck_detector: StuckDetector, event_stream: EventStream
  222. ):
  223. self._impl_syntax_error_events(
  224. event_stream,
  225. error_message='SyntaxError: incomplete input',
  226. random_line=False,
  227. )
  228. with patch('logging.Logger.warning'):
  229. assert stuck_detector.is_stuck() is True
  230. def test_is_not_stuck_incomplete_input_error(
  231. self, stuck_detector: StuckDetector, event_stream: EventStream
  232. ):
  233. self._impl_syntax_error_events(
  234. event_stream,
  235. error_message='SyntaxError: incomplete input',
  236. random_line=True,
  237. )
  238. with patch('logging.Logger.warning'):
  239. assert stuck_detector.is_stuck() is False
  240. def test_is_not_stuck_ipython_unterminated_string_error(
  241. self, stuck_detector: StuckDetector, event_stream: EventStream
  242. ):
  243. self._impl_unterminated_string_error_events(event_stream, random_line=True)
  244. with patch('logging.Logger.warning'):
  245. assert stuck_detector.is_stuck() is False
  246. def test_is_stuck_ipython_unterminated_string_error(
  247. self, stuck_detector: StuckDetector, event_stream: EventStream
  248. ):
  249. self._impl_unterminated_string_error_events(event_stream, random_line=False)
  250. with patch('logging.Logger.warning'):
  251. assert stuck_detector.is_stuck() is True
  252. def test_is_stuck_ipython_syntax_error_not_at_end(
  253. self, stuck_detector: StuckDetector, event_stream: EventStream
  254. ):
  255. # this test is to make sure we don't get false positives
  256. # since the "at line x" is changing in between!
  257. ipython_action_1 = IPythonRunCellAction(code='print("hello')
  258. event_stream.add_event(ipython_action_1, EventSource.AGENT)
  259. ipython_observation_1 = IPythonRunCellObservation(
  260. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 1)\nThis is some additional output',
  261. code='print("hello',
  262. )
  263. ipython_observation_1._cause = ipython_action_1._id
  264. event_stream.add_event(ipython_observation_1, EventSource.USER)
  265. ipython_action_2 = IPythonRunCellAction(code='print("hello')
  266. event_stream.add_event(ipython_action_2, EventSource.AGENT)
  267. ipython_observation_2 = IPythonRunCellObservation(
  268. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 1)\nToo much output here on and on',
  269. code='print("hello',
  270. )
  271. ipython_observation_2._cause = ipython_action_2._id
  272. event_stream.add_event(ipython_observation_2, EventSource.USER)
  273. ipython_action_3 = IPythonRunCellAction(code='print("hello')
  274. event_stream.add_event(ipython_action_3, EventSource.AGENT)
  275. ipython_observation_3 = IPythonRunCellObservation(
  276. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 3)\nEnough',
  277. code='print("hello',
  278. )
  279. ipython_observation_3._cause = ipython_action_3._id
  280. event_stream.add_event(ipython_observation_3, EventSource.USER)
  281. ipython_action_4 = IPythonRunCellAction(code='print("hello')
  282. event_stream.add_event(ipython_action_4, EventSource.AGENT)
  283. ipython_observation_4 = IPythonRunCellObservation(
  284. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 2)\nLast line of output',
  285. code='print("hello',
  286. )
  287. ipython_observation_4._cause = ipython_action_4._id
  288. event_stream.add_event(ipython_observation_4, EventSource.USER)
  289. with patch('logging.Logger.warning') as mock_warning:
  290. assert stuck_detector.is_stuck() is False
  291. mock_warning.assert_not_called()
  292. def test_is_stuck_repeating_action_observation_pattern(
  293. self, stuck_detector: StuckDetector, event_stream: EventStream
  294. ):
  295. message_action = MessageAction(content='Come on', wait_for_response=False)
  296. message_action._source = EventSource.USER
  297. event_stream.add_event(message_action, EventSource.USER)
  298. message_observation = NullObservation(content='')
  299. event_stream.add_event(message_observation, EventSource.USER)
  300. cmd_action_1 = CmdRunAction(command='ls')
  301. event_stream.add_event(cmd_action_1, EventSource.AGENT)
  302. cmd_observation_1 = CmdOutputObservation(
  303. command_id=1, command='ls', content='file1.txt\nfile2.txt'
  304. )
  305. cmd_observation_1._cause = cmd_action_1._id
  306. event_stream.add_event(cmd_observation_1, EventSource.USER)
  307. read_action_1 = FileReadAction(path='file1.txt')
  308. event_stream.add_event(read_action_1, EventSource.AGENT)
  309. read_observation_1 = FileReadObservation(
  310. content='File content', path='file1.txt'
  311. )
  312. read_observation_1._cause = read_action_1._id
  313. event_stream.add_event(read_observation_1, EventSource.USER)
  314. cmd_action_2 = CmdRunAction(command='ls')
  315. event_stream.add_event(cmd_action_2, EventSource.AGENT)
  316. cmd_observation_2 = CmdOutputObservation(
  317. command_id=2, command='ls', content='file1.txt\nfile2.txt'
  318. )
  319. cmd_observation_2._cause = cmd_action_2._id
  320. event_stream.add_event(cmd_observation_2, EventSource.USER)
  321. read_action_2 = FileReadAction(path='file1.txt')
  322. event_stream.add_event(read_action_2, EventSource.AGENT)
  323. read_observation_2 = FileReadObservation(
  324. content='File content', path='file1.txt'
  325. )
  326. read_observation_2._cause = read_action_2._id
  327. event_stream.add_event(read_observation_2, EventSource.USER)
  328. # one more message to break the pattern
  329. message_null_observation = NullObservation(content='')
  330. event_stream.add_event(message_action, EventSource.USER)
  331. event_stream.add_event(message_null_observation, EventSource.USER)
  332. cmd_action_3 = CmdRunAction(command='ls')
  333. event_stream.add_event(cmd_action_3, EventSource.AGENT)
  334. cmd_observation_3 = CmdOutputObservation(
  335. command_id=3, command='ls', content='file1.txt\nfile2.txt'
  336. )
  337. cmd_observation_3._cause = cmd_action_3._id
  338. event_stream.add_event(cmd_observation_3, EventSource.USER)
  339. read_action_3 = FileReadAction(path='file1.txt')
  340. event_stream.add_event(read_action_3, EventSource.AGENT)
  341. read_observation_3 = FileReadObservation(
  342. content='File content', path='file1.txt'
  343. )
  344. read_observation_3._cause = read_action_3._id
  345. event_stream.add_event(read_observation_3, EventSource.USER)
  346. with patch('logging.Logger.warning') as mock_warning:
  347. assert stuck_detector.is_stuck() is True
  348. mock_warning.assert_called_once_with('Action, Observation pattern detected')
  349. def test_is_stuck_not_stuck(
  350. self, stuck_detector: StuckDetector, event_stream: EventStream
  351. ):
  352. message_action = MessageAction(content='Done', wait_for_response=False)
  353. message_action._source = EventSource.USER
  354. hello_action = MessageAction(content='Hello', wait_for_response=False)
  355. event_stream.add_event(hello_action, EventSource.USER)
  356. hello_observation = NullObservation(content='')
  357. hello_observation._cause = hello_action._id
  358. event_stream.add_event(hello_observation, EventSource.USER)
  359. cmd_action_1 = CmdRunAction(command='ls')
  360. event_stream.add_event(cmd_action_1, EventSource.AGENT)
  361. cmd_observation_1 = CmdOutputObservation(
  362. command_id=cmd_action_1.id, command='ls', content='file1.txt\nfile2.txt'
  363. )
  364. cmd_observation_1._cause = cmd_action_1._id
  365. event_stream.add_event(cmd_observation_1, EventSource.USER)
  366. read_action_1 = FileReadAction(path='file1.txt')
  367. event_stream.add_event(read_action_1, EventSource.AGENT)
  368. read_observation_1 = FileReadObservation(
  369. content='File content', path='file1.txt'
  370. )
  371. read_observation_1._cause = read_action_1._id
  372. event_stream.add_event(read_observation_1, EventSource.USER)
  373. cmd_action_2 = CmdRunAction(command='pwd')
  374. event_stream.add_event(cmd_action_2, EventSource.AGENT)
  375. cmd_observation_2 = CmdOutputObservation(
  376. command_id=2, command='pwd', content='/home/user'
  377. )
  378. cmd_observation_2._cause = cmd_action_2._id
  379. event_stream.add_event(cmd_observation_2, EventSource.USER)
  380. read_action_2 = FileReadAction(path='file2.txt')
  381. event_stream.add_event(read_action_2, EventSource.AGENT)
  382. read_observation_2 = FileReadObservation(
  383. content='Another file content', path='file2.txt'
  384. )
  385. read_observation_2._cause = read_action_2._id
  386. event_stream.add_event(read_observation_2, EventSource.USER)
  387. message_null_observation = NullObservation(content='')
  388. event_stream.add_event(message_action, EventSource.USER)
  389. event_stream.add_event(message_null_observation, EventSource.USER)
  390. cmd_action_3 = CmdRunAction(command='pwd')
  391. event_stream.add_event(cmd_action_3, EventSource.AGENT)
  392. cmd_observation_3 = CmdOutputObservation(
  393. command_id=cmd_action_3.id, command='pwd', content='/home/user'
  394. )
  395. cmd_observation_3._cause = cmd_action_3._id
  396. event_stream.add_event(cmd_observation_3, EventSource.USER)
  397. read_action_3 = FileReadAction(path='file2.txt')
  398. event_stream.add_event(read_action_3, EventSource.AGENT)
  399. read_observation_3 = FileReadObservation(
  400. content='Another file content', path='file2.txt'
  401. )
  402. read_observation_3._cause = read_action_3._id
  403. event_stream.add_event(read_observation_3, EventSource.USER)
  404. assert stuck_detector.is_stuck() is False
  405. def test_is_stuck_monologue(self, stuck_detector, event_stream):
  406. # Add events to the event stream
  407. message_action_1 = MessageAction(content='Hi there!')
  408. event_stream.add_event(message_action_1, EventSource.USER)
  409. message_action_1._source = EventSource.USER
  410. message_action_2 = MessageAction(content='Hi there!')
  411. event_stream.add_event(message_action_2, EventSource.AGENT)
  412. message_action_2._source = EventSource.AGENT
  413. message_action_3 = MessageAction(content='How are you?')
  414. event_stream.add_event(message_action_3, EventSource.USER)
  415. message_action_3._source = EventSource.USER
  416. cmd_kill_action = CmdRunAction(
  417. command='echo 42', thought="I'm not stuck, he's stuck"
  418. )
  419. event_stream.add_event(cmd_kill_action, EventSource.AGENT)
  420. message_action_4 = MessageAction(content="I'm doing well, thanks for asking.")
  421. event_stream.add_event(message_action_4, EventSource.AGENT)
  422. message_action_4._source = EventSource.AGENT
  423. message_action_5 = MessageAction(content="I'm doing well, thanks for asking.")
  424. event_stream.add_event(message_action_5, EventSource.AGENT)
  425. message_action_5._source = EventSource.AGENT
  426. message_action_6 = MessageAction(content="I'm doing well, thanks for asking.")
  427. event_stream.add_event(message_action_6, EventSource.AGENT)
  428. message_action_6._source = EventSource.AGENT
  429. assert stuck_detector.is_stuck()
  430. # Add an observation event between the repeated message actions
  431. cmd_output_observation = CmdOutputObservation(
  432. content='OK, I was stuck, but no more.',
  433. command_id=42,
  434. command='storybook',
  435. exit_code=0,
  436. )
  437. cmd_output_observation._cause = cmd_kill_action._id
  438. event_stream.add_event(cmd_output_observation, EventSource.USER)
  439. message_action_7 = MessageAction(content="I'm doing well, thanks for asking.")
  440. event_stream.add_event(message_action_7, EventSource.AGENT)
  441. message_action_7._source = EventSource.AGENT
  442. message_action_8 = MessageAction(content="I'm doing well, thanks for asking.")
  443. event_stream.add_event(message_action_8, EventSource.AGENT)
  444. message_action_8._source = EventSource.AGENT
  445. with patch('logging.Logger.warning'):
  446. assert not stuck_detector.is_stuck()
  447. class TestAgentController:
  448. @pytest.fixture
  449. def controller(self):
  450. controller = Mock(spec=AgentController)
  451. controller._is_stuck = AgentController._is_stuck.__get__(
  452. controller, AgentController
  453. )
  454. controller.delegate = None
  455. controller.state = Mock()
  456. controller.state.history = ShortTermHistory()
  457. return controller
  458. def test_is_stuck_delegate_stuck(self, controller: AgentController):
  459. controller.delegate = Mock()
  460. controller.delegate._is_stuck.return_value = True
  461. assert controller._is_stuck() is True