test_issue_handler.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. from unittest.mock import MagicMock, patch
  2. from openhands.core.config import LLMConfig
  3. from openhands.resolver.github_issue import ReviewThread
  4. from openhands.resolver.issue_definitions import IssueHandler, PRHandler
  5. def test_get_converted_issues_initializes_review_comments():
  6. # Mock the necessary dependencies
  7. with patch('requests.get') as mock_get:
  8. # Mock the response for issues
  9. mock_issues_response = MagicMock()
  10. mock_issues_response.json.return_value = [
  11. {'number': 1, 'title': 'Test Issue', 'body': 'Test Body'}
  12. ]
  13. # Mock the response for comments
  14. mock_comments_response = MagicMock()
  15. mock_comments_response.json.return_value = []
  16. # Set up the mock to return different responses for different calls
  17. # First call is for issues, second call is for comments
  18. mock_get.side_effect = [
  19. mock_issues_response,
  20. mock_comments_response,
  21. mock_comments_response,
  22. ] # Need two comment responses because we make two API calls
  23. # Create an instance of IssueHandler
  24. llm_config = LLMConfig(model='test', api_key='test')
  25. handler = IssueHandler('test-owner', 'test-repo', 'test-token', llm_config)
  26. # Get converted issues
  27. issues = handler.get_converted_issues(issue_numbers=[1])
  28. # Verify that we got exactly one issue
  29. assert len(issues) == 1
  30. # Verify that review_comments is initialized as None
  31. assert issues[0].review_comments is None
  32. # Verify other fields are set correctly
  33. assert issues[0].number == 1
  34. assert issues[0].title == 'Test Issue'
  35. assert issues[0].body == 'Test Body'
  36. assert issues[0].owner == 'test-owner'
  37. assert issues[0].repo == 'test-repo'
  38. def test_get_converted_issues_handles_empty_body():
  39. # Mock the necessary dependencies
  40. with patch('requests.get') as mock_get:
  41. # Mock the response for issues
  42. mock_issues_response = MagicMock()
  43. mock_issues_response.json.return_value = [
  44. {'number': 1, 'title': 'Test Issue', 'body': None}
  45. ]
  46. # Mock the response for comments
  47. mock_comments_response = MagicMock()
  48. mock_comments_response.json.return_value = []
  49. # Set up the mock to return different responses
  50. mock_get.side_effect = [
  51. mock_issues_response,
  52. mock_comments_response,
  53. mock_comments_response,
  54. ]
  55. # Create an instance of IssueHandler
  56. llm_config = LLMConfig(model='test', api_key='test')
  57. handler = IssueHandler('test-owner', 'test-repo', 'test-token', llm_config)
  58. # Get converted issues
  59. issues = handler.get_converted_issues(issue_numbers=[1])
  60. # Verify that we got exactly one issue
  61. assert len(issues) == 1
  62. # Verify that body is empty string when None
  63. assert issues[0].body == ''
  64. # Verify other fields are set correctly
  65. assert issues[0].number == 1
  66. assert issues[0].title == 'Test Issue'
  67. assert issues[0].owner == 'test-owner'
  68. assert issues[0].repo == 'test-repo'
  69. # Verify that review_comments is initialized as None
  70. assert issues[0].review_comments is None
  71. def test_pr_handler_get_converted_issues_with_comments():
  72. # Mock the necessary dependencies
  73. with patch('requests.get') as mock_get:
  74. # Mock the response for PRs
  75. mock_prs_response = MagicMock()
  76. mock_prs_response.json.return_value = [
  77. {
  78. 'number': 1,
  79. 'title': 'Test PR',
  80. 'body': 'Test Body fixes #1',
  81. 'head': {'ref': 'test-branch'},
  82. }
  83. ]
  84. # Mock the response for PR comments
  85. mock_comments_response = MagicMock()
  86. mock_comments_response.json.return_value = [
  87. {'body': 'First comment'},
  88. {'body': 'Second comment'},
  89. ]
  90. # Mock the response for PR metadata (GraphQL)
  91. mock_graphql_response = MagicMock()
  92. mock_graphql_response.json.return_value = {
  93. 'data': {
  94. 'repository': {
  95. 'pullRequest': {
  96. 'closingIssuesReferences': {'edges': []},
  97. 'reviews': {'nodes': []},
  98. 'reviewThreads': {'edges': []},
  99. }
  100. }
  101. }
  102. }
  103. # Set up the mock to return different responses
  104. # We need to return empty responses for subsequent pages
  105. mock_empty_response = MagicMock()
  106. mock_empty_response.json.return_value = []
  107. # Mock the response for fetching the external issue referenced in PR body
  108. mock_external_issue_response = MagicMock()
  109. mock_external_issue_response.json.return_value = {
  110. 'body': 'This is additional context from an externally referenced issue.'
  111. }
  112. mock_get.side_effect = [
  113. mock_prs_response, # First call for PRs
  114. mock_empty_response, # Second call for PRs (empty page)
  115. mock_comments_response, # Third call for PR comments
  116. mock_empty_response, # Fourth call for PR comments (empty page)
  117. mock_external_issue_response, # Mock response for the external issue reference #1
  118. ]
  119. # Mock the post request for GraphQL
  120. with patch('requests.post') as mock_post:
  121. mock_post.return_value = mock_graphql_response
  122. # Create an instance of PRHandler
  123. llm_config = LLMConfig(model='test', api_key='test')
  124. handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
  125. # Get converted issues
  126. prs = handler.get_converted_issues(issue_numbers=[1])
  127. # Verify that we got exactly one PR
  128. assert len(prs) == 1
  129. # Verify that thread_comments are set correctly
  130. assert prs[0].thread_comments == ['First comment', 'Second comment']
  131. # Verify other fields are set correctly
  132. assert prs[0].number == 1
  133. assert prs[0].title == 'Test PR'
  134. assert prs[0].body == 'Test Body fixes #1'
  135. assert prs[0].owner == 'test-owner'
  136. assert prs[0].repo == 'test-repo'
  137. assert prs[0].head_branch == 'test-branch'
  138. assert prs[0].closing_issues == [
  139. 'This is additional context from an externally referenced issue.'
  140. ]
  141. def test_get_issue_comments_with_specific_comment_id():
  142. # Mock the necessary dependencies
  143. with patch('requests.get') as mock_get:
  144. # Mock the response for comments
  145. mock_comments_response = MagicMock()
  146. mock_comments_response.json.return_value = [
  147. {'id': 123, 'body': 'First comment'},
  148. {'id': 456, 'body': 'Second comment'},
  149. ]
  150. mock_get.return_value = mock_comments_response
  151. # Create an instance of IssueHandler
  152. llm_config = LLMConfig(model='test', api_key='test')
  153. handler = IssueHandler('test-owner', 'test-repo', 'test-token', llm_config)
  154. # Get comments with a specific comment_id
  155. specific_comment = handler._get_issue_comments(issue_number=1, comment_id=123)
  156. # Verify only the specific comment is returned
  157. assert specific_comment == ['First comment']
  158. def test_pr_handler_get_converted_issues_with_specific_thread_comment():
  159. # Define the specific comment_id to filter
  160. specific_comment_id = 123
  161. # Mock GraphQL response for review threads
  162. with patch('requests.get') as mock_get:
  163. # Mock the response for PRs
  164. mock_prs_response = MagicMock()
  165. mock_prs_response.json.return_value = [
  166. {
  167. 'number': 1,
  168. 'title': 'Test PR',
  169. 'body': 'Test Body',
  170. 'head': {'ref': 'test-branch'},
  171. }
  172. ]
  173. # Mock the response for PR comments
  174. mock_comments_response = MagicMock()
  175. mock_comments_response.json.return_value = [
  176. {'body': 'First comment', 'id': 123},
  177. {'body': 'Second comment', 'id': 124},
  178. ]
  179. # Mock the response for PR metadata (GraphQL)
  180. mock_graphql_response = MagicMock()
  181. mock_graphql_response.json.return_value = {
  182. 'data': {
  183. 'repository': {
  184. 'pullRequest': {
  185. 'closingIssuesReferences': {'edges': []},
  186. 'reviews': {'nodes': []},
  187. 'reviewThreads': {
  188. 'edges': [
  189. {
  190. 'node': {
  191. 'id': 'review-thread-1',
  192. 'isResolved': False,
  193. 'comments': {
  194. 'nodes': [
  195. {
  196. 'fullDatabaseId': 121,
  197. 'body': 'Specific review comment',
  198. 'path': 'file1.txt',
  199. },
  200. {
  201. 'fullDatabaseId': 456,
  202. 'body': 'Another review comment',
  203. 'path': 'file2.txt',
  204. },
  205. ]
  206. },
  207. }
  208. }
  209. ]
  210. },
  211. }
  212. }
  213. }
  214. }
  215. # Set up the mock to return different responses
  216. # We need to return empty responses for subsequent pages
  217. mock_empty_response = MagicMock()
  218. mock_empty_response.json.return_value = []
  219. mock_get.side_effect = [
  220. mock_prs_response, # First call for PRs
  221. mock_empty_response, # Second call for PRs (empty page)
  222. mock_comments_response, # Third call for PR comments
  223. mock_empty_response, # Fourth call for PR comments (empty page)
  224. ]
  225. # Mock the post request for GraphQL
  226. with patch('requests.post') as mock_post:
  227. mock_post.return_value = mock_graphql_response
  228. # Create an instance of PRHandler
  229. llm_config = LLMConfig(model='test', api_key='test')
  230. handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
  231. # Get converted issues
  232. prs = handler.get_converted_issues(
  233. issue_numbers=[1], comment_id=specific_comment_id
  234. )
  235. # Verify that we got exactly one PR
  236. assert len(prs) == 1
  237. # Verify that thread_comments are set correctly
  238. assert prs[0].thread_comments == ['First comment']
  239. assert prs[0].review_comments == []
  240. assert prs[0].review_threads == []
  241. # Verify other fields are set correctly
  242. assert prs[0].number == 1
  243. assert prs[0].title == 'Test PR'
  244. assert prs[0].body == 'Test Body'
  245. assert prs[0].owner == 'test-owner'
  246. assert prs[0].repo == 'test-repo'
  247. assert prs[0].head_branch == 'test-branch'
  248. def test_pr_handler_get_converted_issues_with_specific_review_thread_comment():
  249. # Define the specific comment_id to filter
  250. specific_comment_id = 123
  251. # Mock GraphQL response for review threads
  252. with patch('requests.get') as mock_get:
  253. # Mock the response for PRs
  254. mock_prs_response = MagicMock()
  255. mock_prs_response.json.return_value = [
  256. {
  257. 'number': 1,
  258. 'title': 'Test PR',
  259. 'body': 'Test Body',
  260. 'head': {'ref': 'test-branch'},
  261. }
  262. ]
  263. # Mock the response for PR comments
  264. mock_comments_response = MagicMock()
  265. mock_comments_response.json.return_value = [
  266. {'body': 'First comment', 'id': 120},
  267. {'body': 'Second comment', 'id': 124},
  268. ]
  269. # Mock the response for PR metadata (GraphQL)
  270. mock_graphql_response = MagicMock()
  271. mock_graphql_response.json.return_value = {
  272. 'data': {
  273. 'repository': {
  274. 'pullRequest': {
  275. 'closingIssuesReferences': {'edges': []},
  276. 'reviews': {'nodes': []},
  277. 'reviewThreads': {
  278. 'edges': [
  279. {
  280. 'node': {
  281. 'id': 'review-thread-1',
  282. 'isResolved': False,
  283. 'comments': {
  284. 'nodes': [
  285. {
  286. 'fullDatabaseId': specific_comment_id,
  287. 'body': 'Specific review comment',
  288. 'path': 'file1.txt',
  289. },
  290. {
  291. 'fullDatabaseId': 456,
  292. 'body': 'Another review comment',
  293. 'path': 'file1.txt',
  294. },
  295. ]
  296. },
  297. }
  298. }
  299. ]
  300. },
  301. }
  302. }
  303. }
  304. }
  305. # Set up the mock to return different responses
  306. # We need to return empty responses for subsequent pages
  307. mock_empty_response = MagicMock()
  308. mock_empty_response.json.return_value = []
  309. mock_get.side_effect = [
  310. mock_prs_response, # First call for PRs
  311. mock_empty_response, # Second call for PRs (empty page)
  312. mock_comments_response, # Third call for PR comments
  313. mock_empty_response, # Fourth call for PR comments (empty page)
  314. ]
  315. # Mock the post request for GraphQL
  316. with patch('requests.post') as mock_post:
  317. mock_post.return_value = mock_graphql_response
  318. # Create an instance of PRHandler
  319. llm_config = LLMConfig(model='test', api_key='test')
  320. handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
  321. # Get converted issues
  322. prs = handler.get_converted_issues(
  323. issue_numbers=[1], comment_id=specific_comment_id
  324. )
  325. # Verify that we got exactly one PR
  326. assert len(prs) == 1
  327. # Verify that thread_comments are set correctly
  328. assert prs[0].thread_comments is None
  329. assert prs[0].review_comments == []
  330. assert len(prs[0].review_threads) == 1
  331. assert isinstance(prs[0].review_threads[0], ReviewThread)
  332. assert (
  333. prs[0].review_threads[0].comment
  334. == 'Specific review comment\n---\nlatest feedback:\nAnother review comment\n'
  335. )
  336. assert prs[0].review_threads[0].files == ['file1.txt']
  337. # Verify other fields are set correctly
  338. assert prs[0].number == 1
  339. assert prs[0].title == 'Test PR'
  340. assert prs[0].body == 'Test Body'
  341. assert prs[0].owner == 'test-owner'
  342. assert prs[0].repo == 'test-repo'
  343. assert prs[0].head_branch == 'test-branch'
  344. def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs():
  345. # Define the specific comment_id to filter
  346. specific_comment_id = 123
  347. # Mock GraphQL response for review threads
  348. with patch('requests.get') as mock_get:
  349. # Mock the response for PRs
  350. mock_prs_response = MagicMock()
  351. mock_prs_response.json.return_value = [
  352. {
  353. 'number': 1,
  354. 'title': 'Test PR fixes #3',
  355. 'body': 'Test Body',
  356. 'head': {'ref': 'test-branch'},
  357. }
  358. ]
  359. # Mock the response for PR comments
  360. mock_comments_response = MagicMock()
  361. mock_comments_response.json.return_value = [
  362. {'body': 'First comment', 'id': 120},
  363. {'body': 'Second comment', 'id': 124},
  364. ]
  365. # Mock the response for PR metadata (GraphQL)
  366. mock_graphql_response = MagicMock()
  367. mock_graphql_response.json.return_value = {
  368. 'data': {
  369. 'repository': {
  370. 'pullRequest': {
  371. 'closingIssuesReferences': {'edges': []},
  372. 'reviews': {'nodes': []},
  373. 'reviewThreads': {
  374. 'edges': [
  375. {
  376. 'node': {
  377. 'id': 'review-thread-1',
  378. 'isResolved': False,
  379. 'comments': {
  380. 'nodes': [
  381. {
  382. 'fullDatabaseId': specific_comment_id,
  383. 'body': 'Specific review comment that references #6',
  384. 'path': 'file1.txt',
  385. },
  386. {
  387. 'fullDatabaseId': 456,
  388. 'body': 'Another review comment referencing #7',
  389. 'path': 'file2.txt',
  390. },
  391. ]
  392. },
  393. }
  394. }
  395. ]
  396. },
  397. }
  398. }
  399. }
  400. }
  401. # Set up the mock to return different responses
  402. # We need to return empty responses for subsequent pages
  403. mock_empty_response = MagicMock()
  404. mock_empty_response.json.return_value = []
  405. # Mock the response for fetching the external issue referenced in PR body
  406. mock_external_issue_response_in_body = MagicMock()
  407. mock_external_issue_response_in_body.json.return_value = {
  408. 'body': 'External context #1.'
  409. }
  410. # Mock the response for fetching the external issue referenced in review thread
  411. mock_external_issue_response_review_thread = MagicMock()
  412. mock_external_issue_response_review_thread.json.return_value = {
  413. 'body': 'External context #2.'
  414. }
  415. mock_get.side_effect = [
  416. mock_prs_response, # First call for PRs
  417. mock_empty_response, # Second call for PRs (empty page)
  418. mock_comments_response, # Third call for PR comments
  419. mock_empty_response, # Fourth call for PR comments (empty page)
  420. mock_external_issue_response_in_body,
  421. mock_external_issue_response_review_thread,
  422. ]
  423. # Mock the post request for GraphQL
  424. with patch('requests.post') as mock_post:
  425. mock_post.return_value = mock_graphql_response
  426. # Create an instance of PRHandler
  427. llm_config = LLMConfig(model='test', api_key='test')
  428. handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
  429. # Get converted issues
  430. prs = handler.get_converted_issues(
  431. issue_numbers=[1], comment_id=specific_comment_id
  432. )
  433. # Verify that we got exactly one PR
  434. assert len(prs) == 1
  435. # Verify that thread_comments are set correctly
  436. assert prs[0].thread_comments is None
  437. assert prs[0].review_comments == []
  438. assert len(prs[0].review_threads) == 1
  439. assert isinstance(prs[0].review_threads[0], ReviewThread)
  440. assert (
  441. prs[0].review_threads[0].comment
  442. == 'Specific review comment that references #6\n---\nlatest feedback:\nAnother review comment referencing #7\n'
  443. )
  444. assert prs[0].closing_issues == [
  445. 'External context #1.',
  446. 'External context #2.',
  447. ] # Only includes references inside comment ID and body PR
  448. # Verify other fields are set correctly
  449. assert prs[0].number == 1
  450. assert prs[0].title == 'Test PR fixes #3'
  451. assert prs[0].body == 'Test Body'
  452. assert prs[0].owner == 'test-owner'
  453. assert prs[0].repo == 'test-repo'
  454. assert prs[0].head_branch == 'test-branch'
  455. def test_pr_handler_get_converted_issues_with_duplicate_issue_refs():
  456. # Mock the necessary dependencies
  457. with patch('requests.get') as mock_get:
  458. # Mock the response for PRs
  459. mock_prs_response = MagicMock()
  460. mock_prs_response.json.return_value = [
  461. {
  462. 'number': 1,
  463. 'title': 'Test PR',
  464. 'body': 'Test Body fixes #1',
  465. 'head': {'ref': 'test-branch'},
  466. }
  467. ]
  468. # Mock the response for PR comments
  469. mock_comments_response = MagicMock()
  470. mock_comments_response.json.return_value = [
  471. {'body': 'First comment addressing #1'},
  472. {'body': 'Second comment addressing #2'},
  473. ]
  474. # Mock the response for PR metadata (GraphQL)
  475. mock_graphql_response = MagicMock()
  476. mock_graphql_response.json.return_value = {
  477. 'data': {
  478. 'repository': {
  479. 'pullRequest': {
  480. 'closingIssuesReferences': {'edges': []},
  481. 'reviews': {'nodes': []},
  482. 'reviewThreads': {'edges': []},
  483. }
  484. }
  485. }
  486. }
  487. # Set up the mock to return different responses
  488. # We need to return empty responses for subsequent pages
  489. mock_empty_response = MagicMock()
  490. mock_empty_response.json.return_value = []
  491. # Mock the response for fetching the external issue referenced in PR body
  492. mock_external_issue_response_in_body = MagicMock()
  493. mock_external_issue_response_in_body.json.return_value = {
  494. 'body': 'External context #1.'
  495. }
  496. # Mock the response for fetching the external issue referenced in review thread
  497. mock_external_issue_response_in_comment = MagicMock()
  498. mock_external_issue_response_in_comment.json.return_value = {
  499. 'body': 'External context #2.'
  500. }
  501. mock_get.side_effect = [
  502. mock_prs_response, # First call for PRs
  503. mock_empty_response, # Second call for PRs (empty page)
  504. mock_comments_response, # Third call for PR comments
  505. mock_empty_response, # Fourth call for PR comments (empty page)
  506. mock_external_issue_response_in_body, # Mock response for the external issue reference #1
  507. mock_external_issue_response_in_comment,
  508. ]
  509. # Mock the post request for GraphQL
  510. with patch('requests.post') as mock_post:
  511. mock_post.return_value = mock_graphql_response
  512. # Create an instance of PRHandler
  513. llm_config = LLMConfig(model='test', api_key='test')
  514. handler = PRHandler('test-owner', 'test-repo', 'test-token', llm_config)
  515. # Get converted issues
  516. prs = handler.get_converted_issues(issue_numbers=[1])
  517. # Verify that we got exactly one PR
  518. assert len(prs) == 1
  519. # Verify that thread_comments are set correctly
  520. assert prs[0].thread_comments == [
  521. 'First comment addressing #1',
  522. 'Second comment addressing #2',
  523. ]
  524. # Verify other fields are set correctly
  525. assert prs[0].number == 1
  526. assert prs[0].title == 'Test PR'
  527. assert prs[0].body == 'Test Body fixes #1'
  528. assert prs[0].owner == 'test-owner'
  529. assert prs[0].repo == 'test-repo'
  530. assert prs[0].head_branch == 'test-branch'
  531. assert prs[0].closing_issues == [
  532. 'External context #1.',
  533. 'External context #2.',
  534. ]