xai-review 0.27.0__py3-none-any.whl → 0.29.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 (150) 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/schema/comments.py +14 -0
  5. ai_review/clients/bitbucket/pr/schema/pull_request.py +1 -5
  6. ai_review/clients/bitbucket/pr/schema/user.py +7 -0
  7. ai_review/clients/github/pr/client.py +35 -4
  8. ai_review/clients/github/pr/schema/comments.py +21 -0
  9. ai_review/clients/github/pr/schema/pull_request.py +1 -4
  10. ai_review/clients/github/pr/schema/user.py +6 -0
  11. ai_review/clients/github/pr/types.py +11 -1
  12. ai_review/clients/gitlab/mr/client.py +32 -1
  13. ai_review/clients/gitlab/mr/schema/changes.py +1 -5
  14. ai_review/clients/gitlab/mr/schema/discussions.py +14 -12
  15. ai_review/clients/gitlab/mr/schema/notes.py +5 -0
  16. ai_review/clients/gitlab/mr/schema/position.py +13 -0
  17. ai_review/clients/gitlab/mr/schema/user.py +7 -0
  18. ai_review/clients/gitlab/mr/types.py +16 -7
  19. ai_review/libs/asynchronous/gather.py +8 -1
  20. ai_review/libs/config/prompt.py +96 -64
  21. ai_review/libs/config/review.py +2 -0
  22. ai_review/libs/llm/output_json_parser.py +60 -0
  23. ai_review/prompts/default_inline_reply.md +10 -0
  24. ai_review/prompts/default_summary_reply.md +14 -0
  25. ai_review/prompts/default_system_inline_reply.md +31 -0
  26. ai_review/prompts/default_system_summary_reply.md +13 -0
  27. ai_review/services/artifacts/schema.py +2 -2
  28. ai_review/services/git/service.py +42 -11
  29. ai_review/services/hook/constants.py +14 -0
  30. ai_review/services/hook/service.py +95 -4
  31. ai_review/services/hook/types.py +18 -2
  32. ai_review/services/prompt/adapter.py +1 -1
  33. ai_review/services/prompt/service.py +49 -3
  34. ai_review/services/prompt/tools.py +21 -0
  35. ai_review/services/prompt/types.py +23 -0
  36. ai_review/services/review/gateway/comment.py +45 -6
  37. ai_review/services/review/gateway/llm.py +2 -1
  38. ai_review/services/review/gateway/types.py +50 -0
  39. ai_review/services/review/internal/inline/service.py +40 -0
  40. ai_review/services/review/internal/inline/types.py +8 -0
  41. ai_review/services/review/internal/inline_reply/schema.py +23 -0
  42. ai_review/services/review/internal/inline_reply/service.py +20 -0
  43. ai_review/services/review/internal/inline_reply/types.py +8 -0
  44. ai_review/services/review/{policy → internal/policy}/service.py +2 -1
  45. ai_review/services/review/internal/policy/types.py +15 -0
  46. ai_review/services/review/{summary → internal/summary}/service.py +2 -2
  47. ai_review/services/review/{summary → internal/summary}/types.py +1 -1
  48. ai_review/services/review/internal/summary_reply/__init__.py +0 -0
  49. ai_review/services/review/internal/summary_reply/schema.py +8 -0
  50. ai_review/services/review/internal/summary_reply/service.py +15 -0
  51. ai_review/services/review/internal/summary_reply/types.py +8 -0
  52. ai_review/services/review/runner/__init__.py +0 -0
  53. ai_review/services/review/runner/context.py +72 -0
  54. ai_review/services/review/runner/inline.py +80 -0
  55. ai_review/services/review/runner/inline_reply.py +80 -0
  56. ai_review/services/review/runner/summary.py +71 -0
  57. ai_review/services/review/runner/summary_reply.py +79 -0
  58. ai_review/services/review/runner/types.py +6 -0
  59. ai_review/services/review/service.py +78 -110
  60. ai_review/services/vcs/bitbucket/adapter.py +27 -0
  61. ai_review/services/vcs/bitbucket/client.py +118 -42
  62. ai_review/services/vcs/github/adapter.py +35 -0
  63. ai_review/services/vcs/github/client.py +105 -44
  64. ai_review/services/vcs/gitlab/adapter.py +28 -0
  65. ai_review/services/vcs/gitlab/client.py +103 -43
  66. ai_review/services/vcs/types.py +34 -0
  67. ai_review/tests/fixtures/clients/bitbucket.py +2 -2
  68. ai_review/tests/fixtures/clients/github.py +35 -6
  69. ai_review/tests/fixtures/clients/gitlab.py +71 -6
  70. ai_review/tests/fixtures/libs/__init__.py +0 -0
  71. ai_review/tests/fixtures/libs/llm/__init__.py +0 -0
  72. ai_review/tests/fixtures/libs/llm/output_json_parser.py +13 -0
  73. ai_review/tests/fixtures/services/hook.py +8 -0
  74. ai_review/tests/fixtures/services/llm.py +8 -5
  75. ai_review/tests/fixtures/services/prompt.py +70 -0
  76. ai_review/tests/fixtures/services/review/base.py +41 -0
  77. ai_review/tests/fixtures/services/review/gateway/__init__.py +0 -0
  78. ai_review/tests/fixtures/services/review/gateway/comment.py +98 -0
  79. ai_review/tests/fixtures/services/review/gateway/llm.py +17 -0
  80. ai_review/tests/fixtures/services/review/internal/__init__.py +0 -0
  81. ai_review/tests/fixtures/services/review/{inline.py → internal/inline.py} +8 -6
  82. ai_review/tests/fixtures/services/review/internal/inline_reply.py +25 -0
  83. ai_review/tests/fixtures/services/review/internal/policy.py +28 -0
  84. ai_review/tests/fixtures/services/review/internal/summary.py +21 -0
  85. ai_review/tests/fixtures/services/review/internal/summary_reply.py +19 -0
  86. ai_review/tests/fixtures/services/review/runner/__init__.py +0 -0
  87. ai_review/tests/fixtures/services/review/runner/context.py +50 -0
  88. ai_review/tests/fixtures/services/review/runner/inline.py +50 -0
  89. ai_review/tests/fixtures/services/review/runner/inline_reply.py +50 -0
  90. ai_review/tests/fixtures/services/review/runner/summary.py +50 -0
  91. ai_review/tests/fixtures/services/review/runner/summary_reply.py +50 -0
  92. ai_review/tests/fixtures/services/vcs.py +23 -0
  93. ai_review/tests/suites/cli/__init__.py +0 -0
  94. ai_review/tests/suites/cli/test_main.py +54 -0
  95. ai_review/tests/suites/libs/config/test_prompt.py +108 -28
  96. ai_review/tests/suites/libs/llm/__init__.py +0 -0
  97. ai_review/tests/suites/libs/llm/test_output_json_parser.py +155 -0
  98. ai_review/tests/suites/services/hook/test_service.py +88 -4
  99. ai_review/tests/suites/services/prompt/test_adapter.py +3 -3
  100. ai_review/tests/suites/services/prompt/test_service.py +102 -58
  101. ai_review/tests/suites/services/prompt/test_tools.py +86 -1
  102. ai_review/tests/suites/services/review/gateway/__init__.py +0 -0
  103. ai_review/tests/suites/services/review/gateway/test_comment.py +253 -0
  104. ai_review/tests/suites/services/review/gateway/test_llm.py +82 -0
  105. ai_review/tests/suites/services/review/internal/__init__.py +0 -0
  106. ai_review/tests/suites/services/review/internal/inline/__init__.py +0 -0
  107. ai_review/tests/suites/services/review/{inline → internal/inline}/test_schema.py +1 -1
  108. ai_review/tests/suites/services/review/internal/inline/test_service.py +81 -0
  109. ai_review/tests/suites/services/review/internal/inline_reply/__init__.py +0 -0
  110. ai_review/tests/suites/services/review/internal/inline_reply/test_schema.py +57 -0
  111. ai_review/tests/suites/services/review/internal/inline_reply/test_service.py +72 -0
  112. ai_review/tests/suites/services/review/internal/policy/__init__.py +0 -0
  113. ai_review/tests/suites/services/review/{policy → internal/policy}/test_service.py +1 -1
  114. ai_review/tests/suites/services/review/internal/summary/__init__.py +0 -0
  115. ai_review/tests/suites/services/review/{summary → internal/summary}/test_schema.py +1 -1
  116. ai_review/tests/suites/services/review/{summary → internal/summary}/test_service.py +2 -2
  117. ai_review/tests/suites/services/review/internal/summary_reply/__init__.py +0 -0
  118. ai_review/tests/suites/services/review/internal/summary_reply/test_schema.py +19 -0
  119. ai_review/tests/suites/services/review/internal/summary_reply/test_service.py +21 -0
  120. ai_review/tests/suites/services/review/runner/__init__.py +0 -0
  121. ai_review/tests/suites/services/review/runner/test_context.py +89 -0
  122. ai_review/tests/suites/services/review/runner/test_inline.py +100 -0
  123. ai_review/tests/suites/services/review/runner/test_inline_reply.py +109 -0
  124. ai_review/tests/suites/services/review/runner/test_summary.py +87 -0
  125. ai_review/tests/suites/services/review/runner/test_summary_reply.py +97 -0
  126. ai_review/tests/suites/services/review/test_service.py +64 -97
  127. ai_review/tests/suites/services/vcs/bitbucket/test_adapter.py +109 -0
  128. ai_review/tests/suites/services/vcs/bitbucket/{test_service.py → test_client.py} +88 -1
  129. ai_review/tests/suites/services/vcs/github/test_adapter.py +162 -0
  130. ai_review/tests/suites/services/vcs/github/{test_service.py → test_client.py} +102 -2
  131. ai_review/tests/suites/services/vcs/gitlab/test_adapter.py +134 -0
  132. ai_review/tests/suites/services/vcs/gitlab/{test_service.py → test_client.py} +113 -3
  133. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/METADATA +8 -5
  134. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/RECORD +146 -72
  135. ai_review/services/review/inline/service.py +0 -54
  136. ai_review/services/review/inline/types.py +0 -11
  137. ai_review/tests/fixtures/services/review/summary.py +0 -19
  138. ai_review/tests/suites/services/review/inline/test_service.py +0 -107
  139. /ai_review/{services/review/inline → libs/llm}/__init__.py +0 -0
  140. /ai_review/services/review/{policy → internal}/__init__.py +0 -0
  141. /ai_review/services/review/{summary → internal/inline}/__init__.py +0 -0
  142. /ai_review/services/review/{inline → internal/inline}/schema.py +0 -0
  143. /ai_review/{tests/suites/services/review/inline → services/review/internal/inline_reply}/__init__.py +0 -0
  144. /ai_review/{tests/suites/services/review → services/review/internal}/policy/__init__.py +0 -0
  145. /ai_review/{tests/suites/services/review → services/review/internal}/summary/__init__.py +0 -0
  146. /ai_review/services/review/{summary → internal/summary}/schema.py +0 -0
  147. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/WHEEL +0 -0
  148. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/entry_points.txt +0 -0
  149. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/licenses/LICENSE +0 -0
  150. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,41 @@
1
+ import pytest
2
+
3
+ from ai_review.services.cost.types import CostServiceProtocol
4
+ from ai_review.services.review.runner.types import ReviewRunnerProtocol
5
+ from ai_review.services.review.service import ReviewService
6
+
7
+
8
+ @pytest.fixture
9
+ def review_service(
10
+ monkeypatch: pytest.MonkeyPatch,
11
+ fake_cost_service: CostServiceProtocol,
12
+ fake_inline_review_runner: ReviewRunnerProtocol,
13
+ fake_context_review_runner: ReviewRunnerProtocol,
14
+ fake_summary_review_runner: ReviewRunnerProtocol,
15
+ fake_inline_reply_review_runner: ReviewRunnerProtocol,
16
+ fake_summary_reply_review_runner: ReviewRunnerProtocol,
17
+ ):
18
+ monkeypatch.setattr("ai_review.services.review.service.CostService", lambda: fake_cost_service)
19
+
20
+ monkeypatch.setattr(
21
+ "ai_review.services.review.service.InlineReviewRunner",
22
+ lambda **_: fake_inline_review_runner
23
+ )
24
+ monkeypatch.setattr(
25
+ "ai_review.services.review.service.ContextReviewRunner",
26
+ lambda **_: fake_context_review_runner
27
+ )
28
+ monkeypatch.setattr(
29
+ "ai_review.services.review.service.SummaryReviewRunner",
30
+ lambda **_: fake_summary_review_runner
31
+ )
32
+ monkeypatch.setattr(
33
+ "ai_review.services.review.service.InlineReplyReviewRunner",
34
+ lambda **_: fake_inline_reply_review_runner
35
+ )
36
+ monkeypatch.setattr(
37
+ "ai_review.services.review.service.SummaryReplyReviewRunner",
38
+ lambda **_: fake_summary_reply_review_runner
39
+ )
40
+
41
+ return ReviewService()
@@ -0,0 +1,98 @@
1
+ from typing import Any
2
+
3
+ import pytest
4
+
5
+ from ai_review.services.review.gateway.comment import ReviewCommentGateway
6
+ from ai_review.services.review.gateway.types import ReviewCommentGatewayProtocol
7
+ from ai_review.services.review.internal.inline.schema import InlineCommentSchema, InlineCommentListSchema
8
+ from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
9
+ from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
10
+ from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
11
+ from ai_review.services.vcs.types import (
12
+ UserSchema,
13
+ ThreadKind,
14
+ ReviewThreadSchema,
15
+ ReviewCommentSchema,
16
+ VCSClientProtocol
17
+ )
18
+
19
+
20
+ class FakeReviewCommentGateway(ReviewCommentGatewayProtocol):
21
+ def __init__(self, responses: dict[str, Any] | None = None):
22
+ self.calls: list[tuple[str, dict]] = []
23
+
24
+ fake_user = UserSchema(id="u1", username="tester", name="Tester")
25
+
26
+ fake_inline_thread = ReviewThreadSchema(
27
+ id="t1",
28
+ kind=ThreadKind.INLINE,
29
+ file="file.py",
30
+ line=5,
31
+ comments=[
32
+ ReviewCommentSchema(
33
+ id="c1",
34
+ body="AI inline comment <!--AI-->",
35
+ file="file.py",
36
+ line=5,
37
+ author=fake_user
38
+ ),
39
+ ReviewCommentSchema(id="c2", body="Developer reply", file="file.py", line=5, author=fake_user),
40
+ ],
41
+ )
42
+
43
+ fake_summary_thread = ReviewThreadSchema(
44
+ id="t2",
45
+ kind=ThreadKind.SUMMARY,
46
+ comments=[
47
+ ReviewCommentSchema(id="c3", body="AI summary comment <!--AI-->", author=fake_user),
48
+ ReviewCommentSchema(id="c4", body="Developer reply", author=fake_user),
49
+ ],
50
+ )
51
+
52
+ self.responses = responses or {
53
+ "get_inline_threads": [fake_inline_thread],
54
+ "get_summary_threads": [fake_summary_thread],
55
+ "has_existing_inline_comments": False,
56
+ "has_existing_summary_comments": False,
57
+ }
58
+
59
+ async def get_inline_threads(self) -> list[ReviewThreadSchema]:
60
+ self.calls.append(("get_inline_threads", {}))
61
+ return self.responses.get("get_inline_threads", [])
62
+
63
+ async def get_summary_threads(self) -> list[ReviewThreadSchema]:
64
+ self.calls.append(("get_summary_threads", {}))
65
+ return self.responses.get("get_summary_threads", [])
66
+
67
+ async def has_existing_inline_comments(self) -> bool:
68
+ self.calls.append(("has_existing_inline_comments", {}))
69
+ return self.responses.get("has_existing_inline_comments", False)
70
+
71
+ async def has_existing_summary_comments(self) -> bool:
72
+ self.calls.append(("has_existing_summary_comments", {}))
73
+ return self.responses.get("has_existing_summary_comments", False)
74
+
75
+ async def process_inline_reply(self, thread_id: str, reply: InlineCommentReplySchema) -> None:
76
+ self.calls.append(("process_inline_reply", {"thread_id": thread_id, "reply": reply}))
77
+
78
+ async def process_summary_reply(self, thread_id: str, reply: SummaryCommentReplySchema) -> None:
79
+ self.calls.append(("process_summary_reply", {"thread_id": thread_id, "reply": reply}))
80
+
81
+ async def process_inline_comment(self, comment: InlineCommentSchema) -> None:
82
+ self.calls.append(("process_inline_comment", {"comment": comment}))
83
+
84
+ async def process_summary_comment(self, comment: SummaryCommentSchema) -> None:
85
+ self.calls.append(("process_summary_comment", {"comment": comment}))
86
+
87
+ async def process_inline_comments(self, comments: InlineCommentListSchema) -> None:
88
+ self.calls.append(("process_inline_comments", {"comments": comments}))
89
+
90
+
91
+ @pytest.fixture
92
+ def fake_review_comment_gateway() -> FakeReviewCommentGateway:
93
+ return FakeReviewCommentGateway()
94
+
95
+
96
+ @pytest.fixture
97
+ def review_comment_gateway(fake_vcs_client: VCSClientProtocol) -> ReviewCommentGateway:
98
+ return ReviewCommentGateway(vcs=fake_vcs_client)
@@ -0,0 +1,17 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol
4
+
5
+
6
+ class FakeReviewLLMGateway(ReviewLLMGatewayProtocol):
7
+ def __init__(self):
8
+ self.calls: list[tuple[str, dict]] = []
9
+
10
+ async def ask(self, prompt: str, prompt_system: str) -> str:
11
+ self.calls.append(("ask", {"prompt": prompt, "prompt_system": prompt_system}))
12
+ return "FAKE_LLM_RESPONSE"
13
+
14
+
15
+ @pytest.fixture
16
+ def fake_review_llm_gateway() -> FakeReviewLLMGateway:
17
+ return FakeReviewLLMGateway()
@@ -1,7 +1,8 @@
1
1
  import pytest
2
2
 
3
- from ai_review.services.review.inline.schema import InlineCommentListSchema, InlineCommentSchema
4
- from ai_review.services.review.inline.types import InlineCommentServiceProtocol
3
+ from ai_review.services.review.internal.inline.schema import InlineCommentListSchema, InlineCommentSchema
4
+ from ai_review.services.review.internal.inline.service import InlineCommentService
5
+ from ai_review.services.review.internal.inline.types import InlineCommentServiceProtocol
5
6
 
6
7
 
7
8
  class FakeInlineCommentService(InlineCommentServiceProtocol):
@@ -15,11 +16,12 @@ class FakeInlineCommentService(InlineCommentServiceProtocol):
15
16
  self.calls.append(("parse_model_output", {"output": output}))
16
17
  return InlineCommentListSchema(root=self.comments)
17
18
 
18
- def try_parse_model_output(self, raw: str) -> InlineCommentListSchema | None:
19
- self.calls.append(("try_parse_model_output", {"raw": raw}))
20
- return InlineCommentListSchema(root=self.comments)
21
-
22
19
 
23
20
  @pytest.fixture
24
21
  def fake_inline_comment_service() -> FakeInlineCommentService:
25
22
  return FakeInlineCommentService()
23
+
24
+
25
+ @pytest.fixture
26
+ def inline_comment_service() -> InlineCommentService:
27
+ return InlineCommentService()
@@ -0,0 +1,25 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
4
+ from ai_review.services.review.internal.inline_reply.service import InlineCommentReplyService
5
+ from ai_review.services.review.internal.inline_reply.types import InlineCommentReplyServiceProtocol
6
+
7
+
8
+ class FakeInlineCommentReplyService(InlineCommentReplyServiceProtocol):
9
+ def __init__(self, reply: InlineCommentReplySchema | None = None):
10
+ self.calls: list[tuple[str, dict]] = []
11
+ self.reply = reply or InlineCommentReplySchema(message="Looks good!", suggestion="use const instead of var")
12
+
13
+ def parse_model_output(self, output: str) -> InlineCommentReplySchema | None:
14
+ self.calls.append(("parse_model_output", {"output": output}))
15
+ return self.reply
16
+
17
+
18
+ @pytest.fixture
19
+ def fake_inline_comment_reply_service() -> FakeInlineCommentReplyService:
20
+ return FakeInlineCommentReplyService()
21
+
22
+
23
+ @pytest.fixture
24
+ def inline_comment_reply_service() -> InlineCommentReplyService:
25
+ return InlineCommentReplyService()
@@ -0,0 +1,28 @@
1
+ from typing import Any
2
+
3
+ import pytest
4
+
5
+ from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
6
+
7
+
8
+ class FakeReviewPolicyService(ReviewPolicyServiceProtocol):
9
+ def __init__(self, responses: dict[str, Any] | None = None):
10
+ self.calls: list[tuple[str, dict]] = []
11
+ self.responses = responses or {}
12
+
13
+ def apply_for_files(self, files: list[str]) -> list[str]:
14
+ self.calls.append(("apply_for_files", {"files": files}))
15
+ return self.responses.get("apply_for_files", files)
16
+
17
+ def apply_for_inline_comments(self, comments: list) -> list:
18
+ self.calls.append(("apply_for_inline_comments", {"comments": comments}))
19
+ return self.responses.get("apply_for_inline_comments", comments)
20
+
21
+ def apply_for_context_comments(self, comments: list) -> list:
22
+ self.calls.append(("apply_for_context_comments", {"comments": comments}))
23
+ return self.responses.get("apply_for_context_comments", comments)
24
+
25
+
26
+ @pytest.fixture
27
+ def fake_review_policy_service() -> FakeReviewPolicyService:
28
+ return FakeReviewPolicyService()
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+ import pytest
4
+
5
+ from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
6
+ from ai_review.services.review.internal.summary.types import SummaryCommentServiceProtocol
7
+
8
+
9
+ class FakeSummaryCommentService(SummaryCommentServiceProtocol):
10
+ def __init__(self, responses: dict[str, Any] | None = None):
11
+ self.calls: list[tuple[str, dict]] = []
12
+ self.responses = responses or {}
13
+
14
+ def parse_model_output(self, output: str) -> SummaryCommentSchema:
15
+ self.calls.append(("parse_model_output", {"output": output}))
16
+ return self.responses.get("parse_model_output", SummaryCommentSchema(text="This is a summary comment"))
17
+
18
+
19
+ @pytest.fixture
20
+ def fake_summary_comment_service() -> FakeSummaryCommentService:
21
+ return FakeSummaryCommentService()
@@ -0,0 +1,19 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
4
+ from ai_review.services.review.internal.summary_reply.types import SummaryCommentReplyServiceProtocol
5
+
6
+
7
+ class FakeSummaryCommentReplyService(SummaryCommentReplyServiceProtocol):
8
+ def __init__(self, reply: SummaryCommentReplySchema | None = None):
9
+ self.calls: list[tuple[str, dict]] = []
10
+ self.reply = reply or SummaryCommentReplySchema(text="Overall, the code looks clean and efficient.")
11
+
12
+ def parse_model_output(self, output: str) -> SummaryCommentReplySchema:
13
+ self.calls.append(("parse_model_output", {"output": output}))
14
+ return self.reply
15
+
16
+
17
+ @pytest.fixture
18
+ def fake_summary_comment_reply_service() -> FakeSummaryCommentReplyService:
19
+ return FakeSummaryCommentReplyService()
@@ -0,0 +1,50 @@
1
+ import pytest
2
+
3
+ from ai_review.services.cost.types import CostServiceProtocol
4
+ from ai_review.services.diff.types import DiffServiceProtocol
5
+ from ai_review.services.git.types import GitServiceProtocol
6
+ from ai_review.services.prompt.types import PromptServiceProtocol
7
+ from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol, ReviewCommentGatewayProtocol
8
+ from ai_review.services.review.internal.inline.types import InlineCommentServiceProtocol
9
+ from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
10
+ from ai_review.services.review.runner.context import ContextReviewRunner
11
+ from ai_review.services.review.runner.types import ReviewRunnerProtocol
12
+ from ai_review.services.vcs.types import VCSClientProtocol
13
+
14
+
15
+ class FakeContextReviewRunner(ReviewRunnerProtocol):
16
+ def __init__(self):
17
+ self.calls = []
18
+
19
+ async def run(self) -> None:
20
+ self.calls.append(("run", {}))
21
+
22
+
23
+ @pytest.fixture
24
+ def fake_context_review_runner() -> FakeContextReviewRunner:
25
+ return FakeContextReviewRunner()
26
+
27
+
28
+ @pytest.fixture
29
+ def context_review_runner(
30
+ fake_vcs_client: VCSClientProtocol,
31
+ fake_git_service: GitServiceProtocol,
32
+ fake_diff_service: DiffServiceProtocol,
33
+ fake_cost_service: CostServiceProtocol,
34
+ fake_prompt_service: PromptServiceProtocol,
35
+ fake_review_llm_gateway: ReviewLLMGatewayProtocol,
36
+ fake_review_policy_service: ReviewPolicyServiceProtocol,
37
+ fake_review_comment_gateway: ReviewCommentGatewayProtocol,
38
+ fake_inline_comment_service: InlineCommentServiceProtocol,
39
+ ) -> ContextReviewRunner:
40
+ return ContextReviewRunner(
41
+ vcs=fake_vcs_client,
42
+ git=fake_git_service,
43
+ diff=fake_diff_service,
44
+ cost=fake_cost_service,
45
+ prompt=fake_prompt_service,
46
+ review_policy=fake_review_policy_service,
47
+ inline_comment=fake_inline_comment_service,
48
+ review_llm_gateway=fake_review_llm_gateway,
49
+ review_comment_gateway=fake_review_comment_gateway,
50
+ )
@@ -0,0 +1,50 @@
1
+ import pytest
2
+
3
+ from ai_review.services.cost.types import CostServiceProtocol
4
+ from ai_review.services.diff.types import DiffServiceProtocol
5
+ from ai_review.services.git.types import GitServiceProtocol
6
+ from ai_review.services.prompt.types import PromptServiceProtocol
7
+ from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol, ReviewCommentGatewayProtocol
8
+ from ai_review.services.review.internal.inline.types import InlineCommentServiceProtocol
9
+ from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
10
+ from ai_review.services.review.runner.inline import InlineReviewRunner
11
+ from ai_review.services.review.runner.types import ReviewRunnerProtocol
12
+ from ai_review.services.vcs.types import VCSClientProtocol
13
+
14
+
15
+ class FakeInlineReviewRunner(ReviewRunnerProtocol):
16
+ def __init__(self):
17
+ self.calls = []
18
+
19
+ async def run(self) -> None:
20
+ self.calls.append(("run", {}))
21
+
22
+
23
+ @pytest.fixture
24
+ def fake_inline_review_runner() -> FakeInlineReviewRunner:
25
+ return FakeInlineReviewRunner()
26
+
27
+
28
+ @pytest.fixture
29
+ def inline_review_runner(
30
+ fake_vcs_client: VCSClientProtocol,
31
+ fake_git_service: GitServiceProtocol,
32
+ fake_diff_service: DiffServiceProtocol,
33
+ fake_cost_service: CostServiceProtocol,
34
+ fake_prompt_service: PromptServiceProtocol,
35
+ fake_review_llm_gateway: ReviewLLMGatewayProtocol,
36
+ fake_review_policy_service: ReviewPolicyServiceProtocol,
37
+ fake_review_comment_gateway: ReviewCommentGatewayProtocol,
38
+ fake_inline_comment_service: InlineCommentServiceProtocol,
39
+ ) -> InlineReviewRunner:
40
+ return InlineReviewRunner(
41
+ vcs=fake_vcs_client,
42
+ git=fake_git_service,
43
+ diff=fake_diff_service,
44
+ cost=fake_cost_service,
45
+ prompt=fake_prompt_service,
46
+ review_policy=fake_review_policy_service,
47
+ inline_comment=fake_inline_comment_service,
48
+ review_llm_gateway=fake_review_llm_gateway,
49
+ review_comment_gateway=fake_review_comment_gateway,
50
+ )
@@ -0,0 +1,50 @@
1
+ import pytest
2
+
3
+ from ai_review.services.cost.types import CostServiceProtocol
4
+ from ai_review.services.diff.types import DiffServiceProtocol
5
+ from ai_review.services.git.types import GitServiceProtocol
6
+ from ai_review.services.prompt.types import PromptServiceProtocol
7
+ from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol, ReviewCommentGatewayProtocol
8
+ from ai_review.services.review.internal.inline_reply.types import InlineCommentReplyServiceProtocol
9
+ from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
10
+ from ai_review.services.review.runner.inline_reply import InlineReplyReviewRunner
11
+ from ai_review.services.review.runner.types import ReviewRunnerProtocol
12
+ from ai_review.services.vcs.types import VCSClientProtocol
13
+
14
+
15
+ class FakeInlineReplyReviewRunner(ReviewRunnerProtocol):
16
+ def __init__(self):
17
+ self.calls = []
18
+
19
+ async def run(self) -> None:
20
+ self.calls.append(("run", {}))
21
+
22
+
23
+ @pytest.fixture
24
+ def fake_inline_reply_review_runner() -> FakeInlineReplyReviewRunner:
25
+ return FakeInlineReplyReviewRunner()
26
+
27
+
28
+ @pytest.fixture
29
+ def inline_reply_review_runner(
30
+ fake_vcs_client: VCSClientProtocol,
31
+ fake_git_service: GitServiceProtocol,
32
+ fake_diff_service: DiffServiceProtocol,
33
+ fake_cost_service: CostServiceProtocol,
34
+ fake_prompt_service: PromptServiceProtocol,
35
+ fake_review_llm_gateway: ReviewLLMGatewayProtocol,
36
+ fake_review_policy_service: ReviewPolicyServiceProtocol,
37
+ fake_review_comment_gateway: ReviewCommentGatewayProtocol,
38
+ fake_inline_comment_reply_service: InlineCommentReplyServiceProtocol,
39
+ ) -> InlineReplyReviewRunner:
40
+ return InlineReplyReviewRunner(
41
+ vcs=fake_vcs_client,
42
+ git=fake_git_service,
43
+ diff=fake_diff_service,
44
+ cost=fake_cost_service,
45
+ prompt=fake_prompt_service,
46
+ review_policy=fake_review_policy_service,
47
+ review_llm_gateway=fake_review_llm_gateway,
48
+ inline_comment_reply=fake_inline_comment_reply_service,
49
+ review_comment_gateway=fake_review_comment_gateway,
50
+ )
@@ -0,0 +1,50 @@
1
+ import pytest
2
+
3
+ from ai_review.services.cost.types import CostServiceProtocol
4
+ from ai_review.services.diff.types import DiffServiceProtocol
5
+ from ai_review.services.git.types import GitServiceProtocol
6
+ from ai_review.services.prompt.types import PromptServiceProtocol
7
+ from ai_review.services.review.gateway.types import ReviewCommentGatewayProtocol, ReviewLLMGatewayProtocol
8
+ from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
9
+ from ai_review.services.review.internal.summary.types import SummaryCommentServiceProtocol
10
+ from ai_review.services.review.runner.summary import SummaryReviewRunner
11
+ from ai_review.services.review.runner.types import ReviewRunnerProtocol
12
+ from ai_review.services.vcs.types import VCSClientProtocol
13
+
14
+
15
+ class FakeSummaryReviewRunner(ReviewRunnerProtocol):
16
+ def __init__(self):
17
+ self.calls = []
18
+
19
+ async def run(self) -> None:
20
+ self.calls.append(("run", {}))
21
+
22
+
23
+ @pytest.fixture
24
+ def fake_summary_review_runner() -> FakeSummaryReviewRunner:
25
+ return FakeSummaryReviewRunner()
26
+
27
+
28
+ @pytest.fixture
29
+ def summary_review_runner(
30
+ fake_vcs_client: VCSClientProtocol,
31
+ fake_git_service: GitServiceProtocol,
32
+ fake_diff_service: DiffServiceProtocol,
33
+ fake_cost_service: CostServiceProtocol,
34
+ fake_prompt_service: PromptServiceProtocol,
35
+ fake_review_llm_gateway: ReviewLLMGatewayProtocol,
36
+ fake_review_policy_service: ReviewPolicyServiceProtocol,
37
+ fake_review_comment_gateway: ReviewCommentGatewayProtocol,
38
+ fake_summary_comment_service: SummaryCommentServiceProtocol,
39
+ ) -> SummaryReviewRunner:
40
+ return SummaryReviewRunner(
41
+ vcs=fake_vcs_client,
42
+ git=fake_git_service,
43
+ diff=fake_diff_service,
44
+ cost=fake_cost_service,
45
+ prompt=fake_prompt_service,
46
+ review_policy=fake_review_policy_service,
47
+ summary_comment=fake_summary_comment_service,
48
+ review_llm_gateway=fake_review_llm_gateway,
49
+ review_comment_gateway=fake_review_comment_gateway,
50
+ )
@@ -0,0 +1,50 @@
1
+ import pytest
2
+
3
+ from ai_review.services.cost.types import CostServiceProtocol
4
+ from ai_review.services.diff.types import DiffServiceProtocol
5
+ from ai_review.services.git.types import GitServiceProtocol
6
+ from ai_review.services.prompt.types import PromptServiceProtocol
7
+ from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol, ReviewCommentGatewayProtocol
8
+ from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
9
+ from ai_review.services.review.internal.summary_reply.types import SummaryCommentReplyServiceProtocol
10
+ from ai_review.services.review.runner.summary_reply import SummaryReplyReviewRunner
11
+ from ai_review.services.review.runner.types import ReviewRunnerProtocol
12
+ from ai_review.services.vcs.types import VCSClientProtocol
13
+
14
+
15
+ class FakeSummaryReplyReviewRunner(ReviewRunnerProtocol):
16
+ def __init__(self):
17
+ self.calls = []
18
+
19
+ async def run(self) -> None:
20
+ self.calls.append(("run", {}))
21
+
22
+
23
+ @pytest.fixture
24
+ def fake_summary_reply_review_runner() -> FakeSummaryReplyReviewRunner:
25
+ return FakeSummaryReplyReviewRunner()
26
+
27
+
28
+ @pytest.fixture
29
+ def summary_reply_review_runner(
30
+ fake_vcs_client: VCSClientProtocol,
31
+ fake_git_service: GitServiceProtocol,
32
+ fake_diff_service: DiffServiceProtocol,
33
+ fake_cost_service: CostServiceProtocol,
34
+ fake_prompt_service: PromptServiceProtocol,
35
+ fake_review_llm_gateway: ReviewLLMGatewayProtocol,
36
+ fake_review_policy_service: ReviewPolicyServiceProtocol,
37
+ fake_review_comment_gateway: ReviewCommentGatewayProtocol,
38
+ fake_summary_comment_reply_service: SummaryCommentReplyServiceProtocol,
39
+ ) -> SummaryReplyReviewRunner:
40
+ return SummaryReplyReviewRunner(
41
+ vcs=fake_vcs_client,
42
+ git=fake_git_service,
43
+ diff=fake_diff_service,
44
+ cost=fake_cost_service,
45
+ prompt=fake_prompt_service,
46
+ review_policy=fake_review_policy_service,
47
+ review_llm_gateway=fake_review_llm_gateway,
48
+ summary_comment_reply=fake_summary_comment_reply_service,
49
+ review_comment_gateway=fake_review_comment_gateway,
50
+ )
@@ -5,6 +5,7 @@ import pytest
5
5
  from ai_review.services.vcs.types import (
6
6
  VCSClientProtocol,
7
7
  ReviewInfoSchema,
8
+ ReviewThreadSchema,
8
9
  ReviewCommentSchema,
9
10
  )
10
11
 
@@ -43,6 +44,28 @@ class FakeVCSClient(VCSClientProtocol):
43
44
 
44
45
  return self.responses.get("create_inline_comment_result", None)
45
46
 
47
+ async def create_inline_reply(self, thread_id: int | str, message: str) -> None:
48
+ self.calls.append(("create_inline_reply", (thread_id, message), {}))
49
+ if error := self.responses.get("create_inline_reply_error"):
50
+ raise error
51
+
52
+ return self.responses.get("create_inline_reply_result", None)
53
+
54
+ async def create_summary_reply(self, thread_id: int | str, message: str) -> None:
55
+ self.calls.append(("create_summary_reply", (thread_id, message), {}))
56
+ if error := self.responses.get("create_summary_reply_error"):
57
+ raise error
58
+
59
+ return self.responses.get("create_summary_reply_result", None)
60
+
61
+ async def get_inline_threads(self) -> list[ReviewThreadSchema]:
62
+ self.calls.append(("get_inline_threads", (), {}))
63
+ return self.responses.get("get_inline_threads", [])
64
+
65
+ async def get_general_threads(self) -> list[ReviewThreadSchema]:
66
+ self.calls.append(("get_general_threads", (), {}))
67
+ return self.responses.get("get_general_threads", [])
68
+
46
69
 
47
70
  @pytest.fixture
48
71
  def fake_vcs_client() -> FakeVCSClient:
File without changes
@@ -0,0 +1,54 @@
1
+ import pytest
2
+ from typer.testing import CliRunner
3
+
4
+ from ai_review.cli.main import app
5
+ from ai_review.services.review.service import ReviewService
6
+
7
+ runner = CliRunner()
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def dummy_review_service(monkeypatch: pytest.MonkeyPatch, review_service: ReviewService):
12
+ monkeypatch.setattr("ai_review.cli.commands.run_review.ReviewService", lambda: review_service)
13
+ monkeypatch.setattr("ai_review.cli.commands.run_inline_review.ReviewService", lambda: review_service)
14
+ monkeypatch.setattr("ai_review.cli.commands.run_context_review.ReviewService", lambda: review_service)
15
+ monkeypatch.setattr("ai_review.cli.commands.run_summary_review.ReviewService", lambda: review_service)
16
+ monkeypatch.setattr("ai_review.cli.commands.run_inline_reply_review.ReviewService", lambda: review_service)
17
+ monkeypatch.setattr("ai_review.cli.commands.run_summary_reply_review.ReviewService", lambda: review_service)
18
+
19
+
20
+ @pytest.mark.parametrize(
21
+ "args, expected_output",
22
+ [
23
+ (["run"], "Starting full AI review..."),
24
+ (["run-inline"], "Starting inline AI review..."),
25
+ (["run-context"], "Starting context AI review..."),
26
+ (["run-summary"], "Starting summary AI review..."),
27
+ (["run-inline-reply"], "Starting inline reply AI review..."),
28
+ (["run-summary-reply"], "Starting summary reply AI review..."),
29
+ ],
30
+ )
31
+ def test_cli_commands_invoke_review_service_successfully(args: list[str], expected_output: str):
32
+ """
33
+ Ensure CLI commands correctly call the ReviewService with fake dependencies.
34
+ """
35
+ result = runner.invoke(app, args)
36
+
37
+ assert result.exit_code == 0
38
+ assert expected_output in result.output
39
+ assert "AI review completed successfully!" in result.output
40
+
41
+
42
+ def test_show_config_outputs_json(monkeypatch: pytest.MonkeyPatch):
43
+ """
44
+ Validate that the 'show-config' command prints settings as JSON.
45
+ """
46
+ monkeypatch.setattr(
47
+ "ai_review.cli.main.settings.model_dump_json",
48
+ lambda **_: '{"debug": true}'
49
+ )
50
+
51
+ result = runner.invoke(app, ["show-config"])
52
+ assert result.exit_code == 0
53
+ assert "Loaded AI Review configuration" in result.output
54
+ assert '{"debug": true}' in result.output