test_issue_handler.py 23 KB

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