test_issue_handler.py 26 KB

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