xai-review 0.26.0__py3-none-any.whl → 0.28.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xai-review might be problematic. Click here for more details.

Files changed (164) hide show
  1. ai_review/cli/commands/run_inline_reply_review.py +7 -0
  2. ai_review/cli/commands/run_summary_reply_review.py +7 -0
  3. ai_review/cli/main.py +17 -0
  4. ai_review/clients/bitbucket/pr/client.py +45 -8
  5. ai_review/clients/bitbucket/pr/schema/comments.py +21 -2
  6. ai_review/clients/bitbucket/pr/schema/files.py +8 -3
  7. ai_review/clients/bitbucket/pr/schema/pull_request.py +1 -5
  8. ai_review/clients/bitbucket/pr/schema/user.py +7 -0
  9. ai_review/clients/bitbucket/tools.py +6 -0
  10. ai_review/clients/github/pr/client.py +98 -13
  11. ai_review/clients/github/pr/schema/comments.py +23 -1
  12. ai_review/clients/github/pr/schema/files.py +2 -1
  13. ai_review/clients/github/pr/schema/pull_request.py +1 -4
  14. ai_review/clients/github/pr/schema/reviews.py +2 -1
  15. ai_review/clients/github/pr/schema/user.py +6 -0
  16. ai_review/clients/github/pr/types.py +11 -1
  17. ai_review/clients/github/tools.py +6 -0
  18. ai_review/clients/gitlab/mr/client.py +67 -7
  19. ai_review/clients/gitlab/mr/schema/changes.py +1 -5
  20. ai_review/clients/gitlab/mr/schema/discussions.py +19 -8
  21. ai_review/clients/gitlab/mr/schema/notes.py +5 -1
  22. ai_review/clients/gitlab/mr/schema/user.py +7 -0
  23. ai_review/clients/gitlab/mr/types.py +16 -7
  24. ai_review/clients/gitlab/tools.py +5 -0
  25. ai_review/libs/config/prompt.py +96 -64
  26. ai_review/libs/config/review.py +2 -0
  27. ai_review/libs/config/vcs/base.py +2 -0
  28. ai_review/libs/config/vcs/pagination.py +6 -0
  29. ai_review/libs/http/paginate.py +43 -0
  30. ai_review/libs/llm/output_json_parser.py +60 -0
  31. ai_review/prompts/default_inline_reply.md +10 -0
  32. ai_review/prompts/default_summary_reply.md +14 -0
  33. ai_review/prompts/default_system_inline_reply.md +31 -0
  34. ai_review/prompts/default_system_summary_reply.md +13 -0
  35. ai_review/services/artifacts/schema.py +2 -2
  36. ai_review/services/hook/constants.py +14 -0
  37. ai_review/services/hook/service.py +95 -4
  38. ai_review/services/hook/types.py +18 -2
  39. ai_review/services/prompt/adapter.py +1 -1
  40. ai_review/services/prompt/service.py +49 -3
  41. ai_review/services/prompt/tools.py +21 -0
  42. ai_review/services/prompt/types.py +23 -0
  43. ai_review/services/review/gateway/comment.py +45 -6
  44. ai_review/services/review/gateway/llm.py +2 -1
  45. ai_review/services/review/gateway/types.py +50 -0
  46. ai_review/services/review/internal/inline/service.py +40 -0
  47. ai_review/services/review/internal/inline/types.py +8 -0
  48. ai_review/services/review/internal/inline_reply/schema.py +23 -0
  49. ai_review/services/review/internal/inline_reply/service.py +20 -0
  50. ai_review/services/review/internal/inline_reply/types.py +8 -0
  51. ai_review/services/review/{policy → internal/policy}/service.py +2 -1
  52. ai_review/services/review/internal/policy/types.py +15 -0
  53. ai_review/services/review/{summary → internal/summary}/service.py +2 -2
  54. ai_review/services/review/{summary → internal/summary}/types.py +1 -1
  55. ai_review/services/review/internal/summary_reply/__init__.py +0 -0
  56. ai_review/services/review/internal/summary_reply/schema.py +8 -0
  57. ai_review/services/review/internal/summary_reply/service.py +15 -0
  58. ai_review/services/review/internal/summary_reply/types.py +8 -0
  59. ai_review/services/review/runner/__init__.py +0 -0
  60. ai_review/services/review/runner/context.py +72 -0
  61. ai_review/services/review/runner/inline.py +80 -0
  62. ai_review/services/review/runner/inline_reply.py +80 -0
  63. ai_review/services/review/runner/summary.py +71 -0
  64. ai_review/services/review/runner/summary_reply.py +79 -0
  65. ai_review/services/review/runner/types.py +6 -0
  66. ai_review/services/review/service.py +78 -110
  67. ai_review/services/vcs/bitbucket/adapter.py +24 -0
  68. ai_review/services/vcs/bitbucket/client.py +107 -42
  69. ai_review/services/vcs/github/adapter.py +35 -0
  70. ai_review/services/vcs/github/client.py +105 -44
  71. ai_review/services/vcs/gitlab/adapter.py +26 -0
  72. ai_review/services/vcs/gitlab/client.py +91 -38
  73. ai_review/services/vcs/types.py +34 -0
  74. ai_review/tests/fixtures/clients/bitbucket.py +2 -2
  75. ai_review/tests/fixtures/clients/github.py +35 -6
  76. ai_review/tests/fixtures/clients/gitlab.py +42 -3
  77. ai_review/tests/fixtures/libs/__init__.py +0 -0
  78. ai_review/tests/fixtures/libs/llm/__init__.py +0 -0
  79. ai_review/tests/fixtures/libs/llm/output_json_parser.py +13 -0
  80. ai_review/tests/fixtures/services/hook.py +8 -0
  81. ai_review/tests/fixtures/services/llm.py +8 -5
  82. ai_review/tests/fixtures/services/prompt.py +70 -0
  83. ai_review/tests/fixtures/services/review/base.py +41 -0
  84. ai_review/tests/fixtures/services/review/gateway/__init__.py +0 -0
  85. ai_review/tests/fixtures/services/review/gateway/comment.py +98 -0
  86. ai_review/tests/fixtures/services/review/gateway/llm.py +17 -0
  87. ai_review/tests/fixtures/services/review/internal/__init__.py +0 -0
  88. ai_review/tests/fixtures/services/review/{inline.py → internal/inline.py} +8 -6
  89. ai_review/tests/fixtures/services/review/internal/inline_reply.py +25 -0
  90. ai_review/tests/fixtures/services/review/internal/policy.py +28 -0
  91. ai_review/tests/fixtures/services/review/internal/summary.py +21 -0
  92. ai_review/tests/fixtures/services/review/internal/summary_reply.py +19 -0
  93. ai_review/tests/fixtures/services/review/runner/__init__.py +0 -0
  94. ai_review/tests/fixtures/services/review/runner/context.py +50 -0
  95. ai_review/tests/fixtures/services/review/runner/inline.py +50 -0
  96. ai_review/tests/fixtures/services/review/runner/inline_reply.py +50 -0
  97. ai_review/tests/fixtures/services/review/runner/summary.py +50 -0
  98. ai_review/tests/fixtures/services/review/runner/summary_reply.py +50 -0
  99. ai_review/tests/fixtures/services/vcs.py +23 -0
  100. ai_review/tests/suites/cli/__init__.py +0 -0
  101. ai_review/tests/suites/cli/test_main.py +54 -0
  102. ai_review/tests/suites/clients/bitbucket/__init__.py +0 -0
  103. ai_review/tests/suites/clients/bitbucket/test_client.py +14 -0
  104. ai_review/tests/suites/clients/bitbucket/test_tools.py +31 -0
  105. ai_review/tests/suites/clients/github/test_tools.py +31 -0
  106. ai_review/tests/suites/clients/gitlab/test_tools.py +26 -0
  107. ai_review/tests/suites/libs/config/test_prompt.py +108 -28
  108. ai_review/tests/suites/libs/http/__init__.py +0 -0
  109. ai_review/tests/suites/libs/http/test_paginate.py +95 -0
  110. ai_review/tests/suites/libs/llm/__init__.py +0 -0
  111. ai_review/tests/suites/libs/llm/test_output_json_parser.py +155 -0
  112. ai_review/tests/suites/services/hook/test_service.py +88 -4
  113. ai_review/tests/suites/services/prompt/test_adapter.py +3 -3
  114. ai_review/tests/suites/services/prompt/test_service.py +102 -58
  115. ai_review/tests/suites/services/prompt/test_tools.py +86 -1
  116. ai_review/tests/suites/services/review/gateway/__init__.py +0 -0
  117. ai_review/tests/suites/services/review/gateway/test_comment.py +253 -0
  118. ai_review/tests/suites/services/review/gateway/test_llm.py +82 -0
  119. ai_review/tests/suites/services/review/internal/__init__.py +0 -0
  120. ai_review/tests/suites/services/review/internal/inline/__init__.py +0 -0
  121. ai_review/tests/suites/services/review/{inline → internal/inline}/test_schema.py +1 -1
  122. ai_review/tests/suites/services/review/internal/inline/test_service.py +81 -0
  123. ai_review/tests/suites/services/review/internal/inline_reply/__init__.py +0 -0
  124. ai_review/tests/suites/services/review/internal/inline_reply/test_schema.py +57 -0
  125. ai_review/tests/suites/services/review/internal/inline_reply/test_service.py +72 -0
  126. ai_review/tests/suites/services/review/internal/policy/__init__.py +0 -0
  127. ai_review/tests/suites/services/review/{policy → internal/policy}/test_service.py +1 -1
  128. ai_review/tests/suites/services/review/internal/summary/__init__.py +0 -0
  129. ai_review/tests/suites/services/review/{summary → internal/summary}/test_schema.py +1 -1
  130. ai_review/tests/suites/services/review/{summary → internal/summary}/test_service.py +2 -2
  131. ai_review/tests/suites/services/review/internal/summary_reply/__init__.py +0 -0
  132. ai_review/tests/suites/services/review/internal/summary_reply/test_schema.py +19 -0
  133. ai_review/tests/suites/services/review/internal/summary_reply/test_service.py +21 -0
  134. ai_review/tests/suites/services/review/runner/__init__.py +0 -0
  135. ai_review/tests/suites/services/review/runner/test_context.py +89 -0
  136. ai_review/tests/suites/services/review/runner/test_inline.py +100 -0
  137. ai_review/tests/suites/services/review/runner/test_inline_reply.py +109 -0
  138. ai_review/tests/suites/services/review/runner/test_summary.py +87 -0
  139. ai_review/tests/suites/services/review/runner/test_summary_reply.py +97 -0
  140. ai_review/tests/suites/services/review/test_service.py +64 -97
  141. ai_review/tests/suites/services/vcs/bitbucket/test_adapter.py +109 -0
  142. ai_review/tests/suites/services/vcs/bitbucket/{test_service.py → test_client.py} +88 -1
  143. ai_review/tests/suites/services/vcs/github/test_adapter.py +162 -0
  144. ai_review/tests/suites/services/vcs/github/{test_service.py → test_client.py} +102 -2
  145. ai_review/tests/suites/services/vcs/gitlab/test_adapter.py +105 -0
  146. ai_review/tests/suites/services/vcs/gitlab/{test_service.py → test_client.py} +99 -1
  147. {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/METADATA +8 -5
  148. {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/RECORD +160 -75
  149. ai_review/services/review/inline/service.py +0 -54
  150. ai_review/services/review/inline/types.py +0 -11
  151. ai_review/tests/fixtures/services/review/summary.py +0 -19
  152. ai_review/tests/suites/services/review/inline/test_service.py +0 -107
  153. /ai_review/{services/review/inline → libs/llm}/__init__.py +0 -0
  154. /ai_review/services/review/{policy → internal}/__init__.py +0 -0
  155. /ai_review/services/review/{summary → internal/inline}/__init__.py +0 -0
  156. /ai_review/services/review/{inline → internal/inline}/schema.py +0 -0
  157. /ai_review/{tests/suites/services/review/inline → services/review/internal/inline_reply}/__init__.py +0 -0
  158. /ai_review/{tests/suites/services/review → services/review/internal}/policy/__init__.py +0 -0
  159. /ai_review/{tests/suites/services/review → services/review/internal}/summary/__init__.py +0 -0
  160. /ai_review/services/review/{summary → internal/summary}/schema.py +0 -0
  161. {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/WHEEL +0 -0
  162. {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/entry_points.txt +0 -0
  163. {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/licenses/LICENSE +0 -0
  164. {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,89 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.runner.context import ContextReviewRunner
4
+ from ai_review.tests.fixtures.services.cost import FakeCostService
5
+ from ai_review.tests.fixtures.services.diff import FakeDiffService
6
+ from ai_review.tests.fixtures.services.prompt import FakePromptService
7
+ from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
8
+ from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
9
+ from ai_review.tests.fixtures.services.review.internal.inline import FakeInlineCommentService
10
+ from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
11
+ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
12
+
13
+
14
+ @pytest.mark.asyncio
15
+ async def test_run_happy_path(
16
+ context_review_runner: ContextReviewRunner,
17
+ fake_vcs_client: FakeVCSClient,
18
+ fake_diff_service: FakeDiffService,
19
+ fake_cost_service: FakeCostService,
20
+ fake_prompt_service: FakePromptService,
21
+ fake_review_llm_gateway: FakeReviewLLMGateway,
22
+ fake_review_policy_service: FakeReviewPolicyService,
23
+ fake_review_comment_gateway: FakeReviewCommentGateway,
24
+ ):
25
+ """Should render all changed files, call LLM and post inline comments."""
26
+ await context_review_runner.run()
27
+
28
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
29
+ assert "get_review_info" in vcs_calls
30
+
31
+ assert any(call[0] == "render_files" for call in fake_diff_service.calls)
32
+ assert any(call[0] == "apply_for_files" for call in fake_review_policy_service.calls)
33
+ assert any(call[0] == "build_context_request" for call in fake_prompt_service.calls)
34
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
35
+ assert any(call[0] == "process_inline_comments" for call in fake_review_comment_gateway.calls)
36
+
37
+ assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_run_skips_when_existing_comments(
42
+ context_review_runner: ContextReviewRunner,
43
+ fake_vcs_client: FakeVCSClient,
44
+ fake_review_llm_gateway: FakeReviewLLMGateway,
45
+ fake_review_comment_gateway: FakeReviewCommentGateway,
46
+ ):
47
+ """Should skip context review if inline comments already exist."""
48
+ fake_review_comment_gateway.responses["has_existing_inline_comments"] = True
49
+
50
+ await context_review_runner.run()
51
+
52
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
53
+ assert vcs_calls == []
54
+ assert not any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
55
+
56
+
57
+ @pytest.mark.asyncio
58
+ async def test_run_skips_when_no_changed_files(
59
+ context_review_runner: ContextReviewRunner,
60
+ fake_vcs_client: FakeVCSClient,
61
+ fake_review_policy_service: FakeReviewPolicyService,
62
+ ):
63
+ """Should skip when no changed files after policy filtering."""
64
+ fake_review_policy_service.responses["apply_for_files"] = []
65
+
66
+ await context_review_runner.run()
67
+
68
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
69
+ assert "get_review_info" in vcs_calls
70
+ assert any(call[0] == "apply_for_files" for call in fake_review_policy_service.calls)
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_run_skips_when_no_comments_after_llm(
75
+ context_review_runner: ContextReviewRunner,
76
+ fake_review_llm_gateway: FakeReviewLLMGateway,
77
+ fake_review_policy_service: FakeReviewPolicyService,
78
+ fake_review_comment_gateway: FakeReviewCommentGateway,
79
+ fake_inline_comment_service: FakeInlineCommentService,
80
+
81
+ ):
82
+ """Should not post comments if LLM output is empty."""
83
+ fake_inline_comment_service.comments = []
84
+
85
+ await context_review_runner.run()
86
+
87
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
88
+ assert any(call[0] == "apply_for_context_comments" for call in fake_review_policy_service.calls)
89
+ assert not any(call[0] == "process_inline_comments" for call in fake_review_comment_gateway.calls)
@@ -0,0 +1,100 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.runner.inline import InlineReviewRunner
4
+ from ai_review.services.vcs.types import ReviewInfoSchema
5
+ from ai_review.tests.fixtures.services.cost import FakeCostService
6
+ from ai_review.tests.fixtures.services.diff import FakeDiffService
7
+ from ai_review.tests.fixtures.services.git import FakeGitService
8
+ from ai_review.tests.fixtures.services.prompt import FakePromptService
9
+ from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
10
+ from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
11
+ from ai_review.tests.fixtures.services.review.internal.inline import FakeInlineCommentService
12
+ from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
13
+ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_run_happy_path(
18
+ inline_review_runner: InlineReviewRunner,
19
+ fake_vcs_client: FakeVCSClient,
20
+ fake_git_service: FakeGitService,
21
+ fake_diff_service: FakeDiffService,
22
+ fake_cost_service: FakeCostService,
23
+ fake_prompt_service: FakePromptService,
24
+ fake_review_llm_gateway: FakeReviewLLMGateway,
25
+ fake_review_policy_service: FakeReviewPolicyService,
26
+ fake_review_comment_gateway: FakeReviewCommentGateway,
27
+ ):
28
+ """Should process all changed files, call LLM and post inline comments."""
29
+ fake_git_service.responses["get_diff_for_file"] = "FAKE_DIFF"
30
+
31
+ await inline_review_runner.run()
32
+
33
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
34
+ assert "get_review_info" in vcs_calls
35
+
36
+ git_calls = [call[0] for call in fake_git_service.calls]
37
+ assert any(call == "get_diff_for_file" for call in git_calls)
38
+
39
+ assert any(call[0] == "render_file" for call in fake_diff_service.calls)
40
+ assert any(call[0] == "apply_for_files" for call in fake_review_policy_service.calls)
41
+ assert any(call[0] == "build_inline_request" for call in fake_prompt_service.calls)
42
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
43
+ assert any(call[0] == "process_inline_comments" for call in fake_review_comment_gateway.calls)
44
+
45
+ assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
46
+
47
+
48
+ @pytest.mark.asyncio
49
+ async def test_run_skips_when_existing_comments(
50
+ inline_review_runner: InlineReviewRunner,
51
+ fake_vcs_client: FakeVCSClient,
52
+ fake_review_llm_gateway: FakeReviewLLMGateway,
53
+ fake_review_comment_gateway: FakeReviewCommentGateway,
54
+ ):
55
+ """Should skip review if there are already existing inline comments."""
56
+ fake_review_comment_gateway.responses["has_existing_inline_comments"] = True
57
+
58
+ await inline_review_runner.run()
59
+
60
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
61
+ assert vcs_calls == []
62
+ assert not any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_process_file_skips_when_no_diff(
67
+ inline_review_runner: InlineReviewRunner,
68
+ fake_git_service: FakeGitService,
69
+ fake_review_llm_gateway: FakeReviewLLMGateway,
70
+ fake_review_comment_gateway: FakeReviewCommentGateway,
71
+ ):
72
+ """Should skip processing file if no diff found."""
73
+ fake_git_service.responses["get_diff_for_file"] = ""
74
+
75
+ review_info = ReviewInfoSchema(base_sha="A", head_sha="B")
76
+ await inline_review_runner.process_file("file.py", review_info)
77
+
78
+ assert not any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
79
+ assert not any(call[0] == "process_inline_comments" for call in fake_review_comment_gateway.calls)
80
+
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_process_file_skips_when_no_comments_after_llm(
84
+ inline_review_runner: InlineReviewRunner,
85
+ fake_git_service: FakeGitService,
86
+ fake_review_llm_gateway: FakeReviewLLMGateway,
87
+ fake_review_policy_service: FakeReviewPolicyService,
88
+ fake_review_comment_gateway: FakeReviewCommentGateway,
89
+ fake_inline_comment_service: FakeInlineCommentService,
90
+ ):
91
+ """Should not post comments if model output produces no inline comments."""
92
+ fake_git_service.responses["get_diff_for_file"] = "SOME_DIFF"
93
+ fake_inline_comment_service.comments = []
94
+
95
+ review_info = ReviewInfoSchema(base_sha="A", head_sha="B")
96
+ await inline_review_runner.process_file("file.py", review_info)
97
+
98
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
99
+ assert any(call[0] == "apply_for_inline_comments" for call in fake_review_policy_service.calls)
100
+ assert not any(call[0] == "process_inline_comments" for call in fake_review_comment_gateway.calls)
@@ -0,0 +1,109 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.runner.inline_reply import InlineReplyReviewRunner
4
+ from ai_review.services.vcs.types import ReviewInfoSchema, ReviewThreadSchema, ReviewCommentSchema, ThreadKind
5
+ from ai_review.tests.fixtures.services.cost import FakeCostService
6
+ from ai_review.tests.fixtures.services.diff import FakeDiffService
7
+ from ai_review.tests.fixtures.services.git import FakeGitService
8
+ from ai_review.tests.fixtures.services.prompt import FakePromptService
9
+ from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
10
+ from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
11
+ from ai_review.tests.fixtures.services.review.internal.inline_reply import FakeInlineCommentReplyService
12
+ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_run_happy_path(
17
+ inline_reply_review_runner: InlineReplyReviewRunner,
18
+ fake_vcs_client: FakeVCSClient,
19
+ fake_git_service: FakeGitService,
20
+ fake_cost_service: FakeCostService,
21
+ fake_diff_service: FakeDiffService,
22
+ fake_prompt_service: FakePromptService,
23
+ fake_review_llm_gateway: FakeReviewLLMGateway,
24
+ fake_review_comment_gateway: FakeReviewCommentGateway,
25
+ ):
26
+ """Should process all threads, call LLM, and post replies."""
27
+ fake_git_service.responses["get_diff_for_file"] = "FAKE_DIFF"
28
+
29
+ await inline_reply_review_runner.run()
30
+
31
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
32
+ assert "get_review_info" in vcs_calls
33
+
34
+ assert any(call[0] == "get_inline_threads" for call in fake_review_comment_gateway.calls)
35
+ assert any(call[0] == "get_diff_for_file" for call in fake_git_service.calls)
36
+ assert any(call[0] == "render_file" for call in fake_diff_service.calls)
37
+ assert any(call[0] == "build_inline_reply_request" for call in fake_prompt_service.calls)
38
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
39
+ assert any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
40
+ assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
41
+
42
+
43
+ @pytest.mark.asyncio
44
+ async def test_run_skips_when_no_threads(
45
+ fake_vcs_client: FakeVCSClient,
46
+ inline_reply_review_runner: InlineReplyReviewRunner,
47
+ fake_review_comment_gateway: FakeReviewCommentGateway,
48
+ ):
49
+ """Should skip when there are no AI inline threads."""
50
+ fake_review_comment_gateway.responses["get_inline_threads"] = []
51
+
52
+ await inline_reply_review_runner.run()
53
+
54
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
55
+ assert "get_review_info" in vcs_calls
56
+ assert any(call[0] == "get_inline_threads" for call in fake_review_comment_gateway.calls)
57
+ assert not any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
58
+
59
+
60
+ @pytest.mark.asyncio
61
+ async def test_process_thread_reply_skips_when_no_diff(
62
+ inline_reply_review_runner: InlineReplyReviewRunner,
63
+ fake_git_service: FakeGitService,
64
+ fake_review_llm_gateway: FakeReviewLLMGateway,
65
+ fake_review_comment_gateway: FakeReviewCommentGateway,
66
+ ):
67
+ """Should skip reply processing when no diff found for file."""
68
+ fake_git_service.responses["get_diff_for_file"] = ""
69
+
70
+ review_info = ReviewInfoSchema(base_sha="A", head_sha="B")
71
+ thread = ReviewThreadSchema(
72
+ id="1",
73
+ kind=ThreadKind.INLINE,
74
+ file="file.py",
75
+ line=1,
76
+ comments=[ReviewCommentSchema(id="c1", body="Some comment")]
77
+ )
78
+
79
+ await inline_reply_review_runner.process_thread_reply(thread, review_info)
80
+
81
+ assert not any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
82
+ assert not any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
83
+
84
+
85
+ @pytest.mark.asyncio
86
+ async def test_process_thread_reply_skips_when_no_reply(
87
+ inline_reply_review_runner: InlineReplyReviewRunner,
88
+ fake_git_service: FakeGitService,
89
+ fake_review_llm_gateway: FakeReviewLLMGateway,
90
+ fake_review_comment_gateway: FakeReviewCommentGateway,
91
+ fake_inline_comment_reply_service: FakeInlineCommentReplyService,
92
+ ):
93
+ """Should not post reply if model output produces no reply schema."""
94
+ fake_git_service.responses["get_diff_for_file"] = "SOME_DIFF"
95
+ fake_inline_comment_reply_service.reply = None
96
+
97
+ review_info = ReviewInfoSchema(base_sha="A", head_sha="B")
98
+ thread = ReviewThreadSchema(
99
+ id="42",
100
+ kind=ThreadKind.INLINE,
101
+ file="main.py",
102
+ line=12,
103
+ comments=[ReviewCommentSchema(id="cm1", body="Fix this!")]
104
+ )
105
+
106
+ await inline_reply_review_runner.process_thread_reply(thread, review_info)
107
+
108
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
109
+ assert not any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
@@ -0,0 +1,87 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
4
+ from ai_review.services.review.runner.summary import SummaryReviewRunner
5
+ from ai_review.tests.fixtures.services.cost import FakeCostService
6
+ from ai_review.tests.fixtures.services.diff import FakeDiffService
7
+ from ai_review.tests.fixtures.services.prompt import FakePromptService
8
+ from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
9
+ from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
10
+ from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
11
+ from ai_review.tests.fixtures.services.review.internal.summary import FakeSummaryCommentService
12
+ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_run_happy_path(
17
+ summary_review_runner: SummaryReviewRunner,
18
+ fake_vcs_client: FakeVCSClient,
19
+ fake_diff_service: FakeDiffService,
20
+ fake_cost_service: FakeCostService,
21
+ fake_prompt_service: FakePromptService,
22
+ fake_review_llm_gateway: FakeReviewLLMGateway,
23
+ fake_review_policy_service: FakeReviewPolicyService,
24
+ fake_review_comment_gateway: FakeReviewCommentGateway,
25
+ ):
26
+ """Should render all changed files, call LLM and post summary comment."""
27
+ await summary_review_runner.run()
28
+
29
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
30
+ assert "get_review_info" in vcs_calls
31
+
32
+ assert any(call[0] == "render_files" for call in fake_diff_service.calls)
33
+ assert any(call[0] == "apply_for_files" for call in fake_review_policy_service.calls)
34
+ assert any(call[0] == "build_summary_request" for call in fake_prompt_service.calls)
35
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
36
+ assert any(call[0] == "process_summary_comment" for call in fake_review_comment_gateway.calls)
37
+
38
+ assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
39
+
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_run_skips_when_existing_summary_comments(
43
+ summary_review_runner: SummaryReviewRunner,
44
+ fake_vcs_client: FakeVCSClient,
45
+ fake_review_llm_gateway: FakeReviewLLMGateway,
46
+ fake_review_comment_gateway: FakeReviewCommentGateway,
47
+ ):
48
+ """Should skip summary review if summary comment already exists."""
49
+ fake_review_comment_gateway.responses["has_existing_summary_comments"] = True
50
+
51
+ await summary_review_runner.run()
52
+
53
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
54
+ assert vcs_calls == []
55
+ assert not any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_run_skips_when_no_changed_files(
60
+ summary_review_runner: SummaryReviewRunner,
61
+ fake_vcs_client: FakeVCSClient,
62
+ fake_review_policy_service: FakeReviewPolicyService,
63
+ ):
64
+ """Should skip when no changed files remain after policy filtering."""
65
+ fake_review_policy_service.responses["apply_for_files"] = []
66
+
67
+ await summary_review_runner.run()
68
+
69
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
70
+ assert "get_review_info" in vcs_calls
71
+ assert any(call[0] == "apply_for_files" for call in fake_review_policy_service.calls)
72
+
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_run_skips_when_empty_summary_from_llm(
76
+ summary_review_runner: SummaryReviewRunner,
77
+ fake_review_llm_gateway: FakeReviewLLMGateway,
78
+ fake_review_comment_gateway: FakeReviewCommentGateway,
79
+ fake_summary_comment_service: FakeSummaryCommentService,
80
+ ):
81
+ """Should skip posting comment if LLM output is empty."""
82
+ fake_summary_comment_service.responses["parse_model_output"] = SummaryCommentSchema(text="")
83
+
84
+ await summary_review_runner.run()
85
+
86
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
87
+ assert not any(call[0] == "process_summary_comment" for call in fake_review_comment_gateway.calls)
@@ -0,0 +1,97 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.runner.summary_reply import SummaryReplyReviewRunner
4
+ from ai_review.services.vcs.types import ReviewInfoSchema, ReviewThreadSchema, ReviewCommentSchema, ThreadKind
5
+ from ai_review.tests.fixtures.services.cost import FakeCostService
6
+ from ai_review.tests.fixtures.services.diff import FakeDiffService
7
+ from ai_review.tests.fixtures.services.prompt import FakePromptService
8
+ from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
9
+ from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
10
+ from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
11
+ from ai_review.tests.fixtures.services.review.internal.summary_reply import FakeSummaryCommentReplyService
12
+ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_run_happy_path(
17
+ summary_reply_review_runner: SummaryReplyReviewRunner,
18
+ fake_vcs_client: FakeVCSClient,
19
+ fake_diff_service: FakeDiffService,
20
+ fake_cost_service: FakeCostService,
21
+ fake_prompt_service: FakePromptService,
22
+ fake_review_llm_gateway: FakeReviewLLMGateway,
23
+ fake_review_comment_gateway: FakeReviewCommentGateway,
24
+ ):
25
+ """Should process all summary threads, call LLM, and post replies."""
26
+ await summary_reply_review_runner.run()
27
+
28
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
29
+ assert "get_review_info" in vcs_calls
30
+
31
+ assert any(call[0] == "get_summary_threads" for call in fake_review_comment_gateway.calls)
32
+ assert any(call[0] == "render_files" for call in fake_diff_service.calls)
33
+ assert any(call[0] == "build_summary_reply_request" for call in fake_prompt_service.calls)
34
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
35
+ assert any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)
36
+ assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_run_skips_when_no_threads(
41
+ summary_reply_review_runner: SummaryReplyReviewRunner,
42
+ fake_review_comment_gateway: FakeReviewCommentGateway,
43
+ fake_vcs_client: FakeVCSClient,
44
+ ):
45
+ """Should skip when there are no AI summary threads."""
46
+ fake_review_comment_gateway.responses["get_summary_threads"] = []
47
+
48
+ await summary_reply_review_runner.run()
49
+
50
+ vcs_calls = [call[0] for call in fake_vcs_client.calls]
51
+ assert "get_review_info" in vcs_calls
52
+ assert any(call[0] == "get_summary_threads" for call in fake_review_comment_gateway.calls)
53
+ assert not any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)
54
+
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_process_thread_reply_skips_when_no_allowed_files(
58
+ summary_reply_review_runner: SummaryReplyReviewRunner,
59
+ fake_review_policy_service: FakeReviewPolicyService,
60
+ fake_review_comment_gateway: FakeReviewCommentGateway,
61
+ ):
62
+ """Should skip processing thread if policy filtered out all files."""
63
+ fake_review_policy_service.responses["apply_for_files"] = []
64
+
65
+ review_info = ReviewInfoSchema(base_sha="A", head_sha="B", changed_files=["a.py"])
66
+ thread = ReviewThreadSchema(
67
+ id="99",
68
+ kind=ThreadKind.SUMMARY,
69
+ comments=[ReviewCommentSchema(id="c1", body="Summary comment")],
70
+ )
71
+
72
+ await summary_reply_review_runner.process_thread_reply(thread, review_info)
73
+
74
+ assert not any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)
75
+
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_process_thread_reply_skips_when_no_reply(
79
+ summary_reply_review_runner: SummaryReplyReviewRunner,
80
+ fake_summary_comment_reply_service: FakeSummaryCommentReplyService,
81
+ fake_review_llm_gateway: FakeReviewLLMGateway,
82
+ fake_review_comment_gateway: FakeReviewCommentGateway,
83
+ ):
84
+ """Should not post reply if model output produced no reply schema."""
85
+ fake_summary_comment_reply_service.reply = None
86
+
87
+ review_info = ReviewInfoSchema(base_sha="A", head_sha="B", changed_files=["x.py"])
88
+ thread = ReviewThreadSchema(
89
+ id="42",
90
+ kind=ThreadKind.SUMMARY,
91
+ comments=[ReviewCommentSchema(id="cm1", body="AI summary comment")],
92
+ )
93
+
94
+ await summary_reply_review_runner.process_thread_reply(thread, review_info)
95
+
96
+ assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
97
+ assert not any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)