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
@@ -12,6 +12,7 @@ from ai_review.clients.gitlab.mr.schema.discussions import (
12
12
  GitLabGetMRDiscussionsResponseSchema,
13
13
  GitLabCreateMRDiscussionRequestSchema,
14
14
  GitLabCreateMRDiscussionResponseSchema,
15
+ GitLabCreateMRDiscussionReplyResponseSchema, GitLabDiscussionPositionSchema,
15
16
  )
16
17
  from ai_review.clients.gitlab.mr.schema.notes import (
17
18
  GitLabNoteSchema,
@@ -60,8 +61,16 @@ class FakeGitLabMergeRequestsHTTPClient(GitLabMergeRequestsHTTPClientProtocol):
60
61
  self.calls.append(("get_notes", {"project_id": project_id, "merge_request_id": merge_request_id}))
61
62
  return GitLabGetMRNotesResponseSchema(
62
63
  root=[
63
- GitLabNoteSchema(id=1, body="General comment"),
64
- GitLabNoteSchema(id=2, body="Another note"),
64
+ GitLabNoteSchema(
65
+ id=1,
66
+ body="General comment",
67
+ author=GitLabUserSchema(id=301, name="Charlie", username="charlie"),
68
+ ),
69
+ GitLabNoteSchema(
70
+ id=2,
71
+ body="Another note",
72
+ author=GitLabUserSchema(id=302, name="Diana", username="diana"),
73
+ ),
65
74
  ]
66
75
  )
67
76
 
@@ -75,6 +84,13 @@ class FakeGitLabMergeRequestsHTTPClient(GitLabMergeRequestsHTTPClientProtocol):
75
84
  GitLabNoteSchema(id=10, body="Inline comment A"),
76
85
  GitLabNoteSchema(id=11, body="Inline comment B"),
77
86
  ],
87
+ position=GitLabDiscussionPositionSchema(
88
+ base_sha="abc123",
89
+ head_sha="def456",
90
+ start_sha="ghi789",
91
+ new_path="src/app.py",
92
+ new_line=12,
93
+ ),
78
94
  )
79
95
  ]
80
96
  )
@@ -100,7 +116,30 @@ class FakeGitLabMergeRequestsHTTPClient(GitLabMergeRequestsHTTPClientProtocol):
100
116
  {"project_id": project_id, "merge_request_id": merge_request_id, "body": request.body}
101
117
  )
102
118
  )
103
- return GitLabCreateMRDiscussionResponseSchema(id="discussion-new", body=request.body)
119
+ return GitLabCreateMRDiscussionResponseSchema(
120
+ id="discussion-new",
121
+ notes=[GitLabNoteSchema(id=1, body=request.body)]
122
+ )
123
+
124
+ async def create_discussion_reply(
125
+ self,
126
+ project_id: str,
127
+ merge_request_id: str,
128
+ discussion_id: str,
129
+ body: str,
130
+ ) -> GitLabCreateMRDiscussionReplyResponseSchema:
131
+ self.calls.append(
132
+ (
133
+ "create_discussion_reply",
134
+ {
135
+ "project_id": project_id,
136
+ "merge_request_id": merge_request_id,
137
+ "discussion_id": discussion_id,
138
+ "body": body,
139
+ },
140
+ )
141
+ )
142
+ return GitLabCreateMRDiscussionReplyResponseSchema(id=100, body=body)
104
143
 
105
144
 
106
145
  class FakeGitLabHTTPClient:
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ import pytest
2
+ from pydantic import BaseModel
3
+
4
+ from ai_review.libs.llm.output_json_parser import LLMOutputJSONParser
5
+
6
+
7
+ class DummyModel(BaseModel):
8
+ text: str
9
+
10
+
11
+ @pytest.fixture
12
+ def llm_output_json_parser() -> LLMOutputJSONParser:
13
+ return LLMOutputJSONParser(DummyModel)
@@ -0,0 +1,8 @@
1
+ import pytest
2
+
3
+ from ai_review.services.hook import HookService
4
+
5
+
6
+ @pytest.fixture
7
+ def hook_service() -> HookService:
8
+ return HookService()
@@ -13,11 +13,14 @@ class FakeLLMClient(LLMClientProtocol):
13
13
  async def chat(self, prompt: str, prompt_system: str) -> ChatResultSchema:
14
14
  self.calls.append(("chat", {"prompt": prompt, "prompt_system": prompt_system}))
15
15
 
16
- return ChatResultSchema(
17
- text=self.responses.get("text", "FAKE_RESPONSE"),
18
- total_tokens=self.responses.get("total_tokens", 42),
19
- prompt_tokens=self.responses.get("prompt_tokens", 21),
20
- completion_tokens=self.responses.get("completion_tokens", 21),
16
+ return self.responses.get(
17
+ "chat",
18
+ ChatResultSchema(
19
+ text="FAKE_RESPONSE",
20
+ total_tokens=42,
21
+ prompt_tokens=21,
22
+ completion_tokens=21,
23
+ )
21
24
  )
22
25
 
23
26
 
@@ -1,8 +1,10 @@
1
1
  import pytest
2
2
 
3
+ from ai_review.libs.config.prompt import PromptConfig
3
4
  from ai_review.services.diff.schema import DiffFileSchema
4
5
  from ai_review.services.prompt.schema import PromptContextSchema
5
6
  from ai_review.services.prompt.types import PromptServiceProtocol
7
+ from ai_review.services.vcs.types import ReviewThreadSchema
6
8
 
7
9
 
8
10
  class FakePromptService(PromptServiceProtocol):
@@ -25,6 +27,24 @@ class FakePromptService(PromptServiceProtocol):
25
27
  self.calls.append(("build_context_request", {"diffs": diffs, "context": context}))
26
28
  return "CONTEXT_PROMPT"
27
29
 
30
+ def build_inline_reply_request(
31
+ self,
32
+ diff: DiffFileSchema,
33
+ thread: ReviewThreadSchema,
34
+ context: PromptContextSchema
35
+ ) -> str:
36
+ self.calls.append(("build_inline_reply_request", {"diff": diff, "thread": thread, "context": context}))
37
+ return f"INLINE_REPLY_PROMPT_FOR_{diff.file}"
38
+
39
+ def build_summary_reply_request(
40
+ self,
41
+ diffs: list[DiffFileSchema],
42
+ thread: ReviewThreadSchema,
43
+ context: PromptContextSchema
44
+ ) -> str:
45
+ self.calls.append(("build_summary_reply_request", {"diffs": diffs, "thread": thread, "context": context}))
46
+ return "SUMMARY_REPLY_PROMPT"
47
+
28
48
  def build_system_inline_request(self, context: PromptContextSchema) -> str:
29
49
  self.calls.append(("build_system_inline_request", {"context": context}))
30
50
  return "SYSTEM_INLINE_PROMPT"
@@ -37,7 +57,57 @@ class FakePromptService(PromptServiceProtocol):
37
57
  self.calls.append(("build_system_summary_request", {"context": context}))
38
58
  return "SYSTEM_SUMMARY_PROMPT"
39
59
 
60
+ def build_system_inline_reply_request(self, context: PromptContextSchema) -> str:
61
+ self.calls.append(("build_system_inline_reply_request", {"context": context}))
62
+ return "SYSTEM_INLINE_REPLY_PROMPT"
63
+
64
+ def build_system_summary_reply_request(self, context: PromptContextSchema) -> str:
65
+ self.calls.append(("build_system_summary_reply_request", {"context": context}))
66
+ return "SYSTEM_SUMMARY_REPLY_PROMPT"
67
+
40
68
 
41
69
  @pytest.fixture
42
70
  def fake_prompt_service() -> FakePromptService:
43
71
  return FakePromptService()
72
+
73
+
74
+ @pytest.fixture
75
+ def fake_prompts(monkeypatch: pytest.MonkeyPatch) -> None:
76
+ """Patch methods of settings.prompt to return dummy values."""
77
+ monkeypatch.setattr(PromptConfig, "load_inline", lambda self: ["GLOBAL_INLINE", "INLINE_PROMPT"])
78
+ monkeypatch.setattr(PromptConfig, "load_context", lambda self: ["GLOBAL_CONTEXT", "CONTEXT_PROMPT"])
79
+ monkeypatch.setattr(PromptConfig, "load_summary", lambda self: ["GLOBAL_SUMMARY", "SUMMARY_PROMPT"])
80
+ monkeypatch.setattr(PromptConfig, "load_system_inline", lambda self: ["SYS_INLINE_A", "SYS_INLINE_B"])
81
+ monkeypatch.setattr(PromptConfig, "load_system_context", lambda self: ["SYS_CONTEXT_A", "SYS_CONTEXT_B"])
82
+ monkeypatch.setattr(PromptConfig, "load_system_summary", lambda self: ["SYS_SUMMARY_A", "SYS_SUMMARY_B"])
83
+ monkeypatch.setattr(PromptConfig, "load_inline_reply", lambda self: ["INLINE_REPLY_A", "INLINE_REPLY_B"])
84
+ monkeypatch.setattr(PromptConfig, "load_summary_reply", lambda self: ["SUMMARY_REPLY_A", "SUMMARY_REPLY_B"])
85
+ monkeypatch.setattr(
86
+ PromptConfig,
87
+ "load_system_inline_reply",
88
+ lambda self: ["SYS_INLINE_REPLY_A", "SYS_INLINE_REPLY_B"]
89
+ )
90
+ monkeypatch.setattr(
91
+ PromptConfig,
92
+ "load_system_summary_reply",
93
+ lambda self: ["SYS_SUMMARY_REPLY_A", "SYS_SUMMARY_REPLY_B"]
94
+ )
95
+
96
+
97
+ @pytest.fixture
98
+ def fake_prompt_context() -> PromptContextSchema:
99
+ """Builds a context object that reflects the new unified review schema."""
100
+ return PromptContextSchema(
101
+ review_title="Fix login bug",
102
+ review_description="Some description",
103
+ review_author_name="Nikita",
104
+ review_author_username="nikita.filonov",
105
+ review_reviewers=["Alice", "Bob"],
106
+ review_reviewers_usernames=["alice", "bob"],
107
+ review_assignees=["Charlie"],
108
+ review_assignees_usernames=["charlie"],
109
+ source_branch="feature/login-fix",
110
+ target_branch="main",
111
+ labels=["bug", "critical"],
112
+ changed_files=["foo.py", "bar.py"],
113
+ )
@@ -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
+ )