xai-review 0.28.0__py3-none-any.whl → 0.30.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 (36) hide show
  1. ai_review/clients/gitlab/mr/schema/discussions.py +3 -11
  2. ai_review/clients/gitlab/mr/schema/notes.py +2 -0
  3. ai_review/clients/gitlab/mr/schema/position.py +13 -0
  4. ai_review/libs/asynchronous/gather.py +8 -1
  5. ai_review/libs/config/review.py +1 -0
  6. ai_review/services/git/service.py +42 -11
  7. ai_review/services/review/gateway/review_dry_run_comment_gateway.py +42 -0
  8. ai_review/services/review/service.py +9 -3
  9. ai_review/services/vcs/bitbucket/adapter.py +5 -2
  10. ai_review/services/vcs/bitbucket/client.py +23 -12
  11. ai_review/services/vcs/gitlab/adapter.py +4 -2
  12. ai_review/services/vcs/gitlab/client.py +24 -17
  13. ai_review/tests/fixtures/clients/gitlab.py +31 -5
  14. ai_review/tests/fixtures/services/review/gateway/{comment.py → review_comment_gateway.py} +1 -1
  15. ai_review/tests/fixtures/services/review/gateway/review_dry_run_comment_gateway.py +103 -0
  16. ai_review/tests/fixtures/services/review/gateway/review_llm_gateway.py +34 -0
  17. ai_review/tests/suites/services/review/gateway/{test_comment.py → test_review_comment_gateway.py} +1 -1
  18. ai_review/tests/suites/services/review/gateway/test_review_dry_run_comment_gateway.py +93 -0
  19. ai_review/tests/suites/services/review/gateway/{test_llm.py → test_review_llm_gateway.py} +1 -15
  20. ai_review/tests/suites/services/review/runner/test_context.py +2 -2
  21. ai_review/tests/suites/services/review/runner/test_inline.py +2 -2
  22. ai_review/tests/suites/services/review/runner/test_inline_reply.py +2 -2
  23. ai_review/tests/suites/services/review/runner/test_summary.py +2 -2
  24. ai_review/tests/suites/services/review/runner/test_summary_reply.py +2 -2
  25. ai_review/tests/suites/services/review/test_service.py +18 -0
  26. ai_review/tests/suites/services/vcs/gitlab/test_adapter.py +35 -6
  27. ai_review/tests/suites/services/vcs/gitlab/test_client.py +24 -12
  28. {xai_review-0.28.0.dist-info → xai_review-0.30.0.dist-info}/METADATA +2 -2
  29. {xai_review-0.28.0.dist-info → xai_review-0.30.0.dist-info}/RECORD +35 -31
  30. ai_review/tests/fixtures/services/review/gateway/llm.py +0 -17
  31. /ai_review/services/review/gateway/{comment.py → review_comment_gateway.py} +0 -0
  32. /ai_review/services/review/gateway/{llm.py → review_llm_gateway.py} +0 -0
  33. {xai_review-0.28.0.dist-info → xai_review-0.30.0.dist-info}/WHEEL +0 -0
  34. {xai_review-0.28.0.dist-info → xai_review-0.30.0.dist-info}/entry_points.txt +0 -0
  35. {xai_review-0.28.0.dist-info → xai_review-0.30.0.dist-info}/licenses/LICENSE +0 -0
  36. {xai_review-0.28.0.dist-info → xai_review-0.30.0.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,13 @@
1
1
  from pydantic import BaseModel, RootModel, Field
2
2
 
3
3
  from ai_review.clients.gitlab.mr.schema.notes import GitLabNoteSchema
4
-
5
-
6
- class GitLabDiscussionPositionSchema(BaseModel):
7
- position_type: str = "text"
8
- base_sha: str
9
- head_sha: str
10
- start_sha: str
11
- new_path: str
12
- new_line: int
4
+ from ai_review.clients.gitlab.mr.schema.position import GitLabPositionSchema
13
5
 
14
6
 
15
7
  class GitLabDiscussionSchema(BaseModel):
16
8
  id: str
17
9
  notes: list[GitLabNoteSchema]
18
- position: GitLabDiscussionPositionSchema | None = None
10
+ position: GitLabPositionSchema | None = None
19
11
 
20
12
 
21
13
  class GitLabGetMRDiscussionsQuerySchema(BaseModel):
@@ -29,7 +21,7 @@ class GitLabGetMRDiscussionsResponseSchema(RootModel[list[GitLabDiscussionSchema
29
21
 
30
22
  class GitLabCreateMRDiscussionRequestSchema(BaseModel):
31
23
  body: str
32
- position: GitLabDiscussionPositionSchema
24
+ position: GitLabPositionSchema
33
25
 
34
26
 
35
27
  class GitLabCreateMRDiscussionResponseSchema(BaseModel):
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, RootModel
2
2
 
3
+ from ai_review.clients.gitlab.mr.schema.position import GitLabPositionSchema
3
4
  from ai_review.clients.gitlab.mr.schema.user import GitLabUserSchema
4
5
 
5
6
 
@@ -7,6 +8,7 @@ class GitLabNoteSchema(BaseModel):
7
8
  id: int
8
9
  body: str
9
10
  author: GitLabUserSchema | None = None
11
+ position: GitLabPositionSchema | None = None
10
12
 
11
13
 
12
14
  class GitLabGetMRNotesQuerySchema(BaseModel):
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class GitLabPositionSchema(BaseModel):
5
+ position_type: str | None = "text"
6
+ base_sha: str | None = None
7
+ head_sha: str | None = None
8
+ start_sha: str | None = None
9
+ old_path: str | None = None
10
+ new_path: str | None = None
11
+ old_line: int | None = None
12
+ new_line: int | None = None
13
+ line_range: dict | None = None
@@ -2,6 +2,9 @@ import asyncio
2
2
  from typing import Awaitable, Iterable, TypeVar
3
3
 
4
4
  from ai_review.config import settings
5
+ from ai_review.libs.logger import get_logger
6
+
7
+ logger = get_logger("GATHER")
5
8
 
6
9
  T = TypeVar("T")
7
10
 
@@ -11,7 +14,11 @@ async def bounded_gather(coroutines: Iterable[Awaitable[T]]) -> tuple[T, ...]:
11
14
 
12
15
  async def wrap(coro: Awaitable[T]) -> T:
13
16
  async with sem:
14
- return await coro
17
+ try:
18
+ return await coro
19
+ except Exception as error:
20
+ logger.warning(f"Task failed: {type(error).__name__}: {error}")
21
+ return error
15
22
 
16
23
  results = await asyncio.gather(*(wrap(coroutine) for coroutine in coroutines), return_exceptions=True)
17
24
  return tuple(results)
@@ -19,6 +19,7 @@ class ReviewMode(StrEnum):
19
19
 
20
20
  class ReviewConfig(BaseModel):
21
21
  mode: ReviewMode = ReviewMode.FULL_FILE_DIFF
22
+ dry_run: bool = False
22
23
  inline_tag: str = Field(default="#ai-review-inline")
23
24
  inline_reply_tag: str = Field(default="#ai-review-inline-reply")
24
25
  summary_tag: str = Field(default="#ai-review-summary")
@@ -1,35 +1,66 @@
1
1
  import subprocess
2
2
  from pathlib import Path
3
3
 
4
+ from ai_review.libs.logger import get_logger
4
5
  from ai_review.services.git.types import GitServiceProtocol
5
6
 
7
+ logger = get_logger("GIT_SERVICE")
8
+
6
9
 
7
10
  class GitService(GitServiceProtocol):
8
11
  def __init__(self, repo_dir: str = "."):
9
12
  self.repo_dir = Path(repo_dir)
10
13
 
11
14
  def run_git(self, *args: str) -> str:
12
- result = subprocess.run(
13
- ["git", *args],
14
- cwd=self.repo_dir,
15
- capture_output=True,
16
- text=True,
17
- check=True,
18
- )
19
- return result.stdout
15
+ cmd = ["git", *args]
16
+ logger.debug(f"Running git command: {' '.join(cmd)} (cwd={self.repo_dir})")
17
+
18
+ try:
19
+ result = subprocess.run(
20
+ cmd,
21
+ cwd=self.repo_dir,
22
+ capture_output=True,
23
+ text=True,
24
+ check=True,
25
+ )
26
+ if result.stderr.strip():
27
+ logger.debug(f"Git stderr: {result.stderr.strip()}")
28
+ return result.stdout
29
+ except subprocess.CalledProcessError as error:
30
+ logger.warning(
31
+ f"Git command failed (exit={error.returncode}): {' '.join(cmd)}\n"
32
+ f"stderr: {error.stderr.strip()}"
33
+ )
34
+ raise
20
35
 
21
36
  def get_diff(self, base_sha: str, head_sha: str, unified: int = 3) -> str:
22
37
  return self.run_git("diff", f"--unified={unified}", base_sha, head_sha)
23
38
 
24
39
  def get_diff_for_file(self, base_sha: str, head_sha: str, file: str, unified: int = 3) -> str:
25
- return self.run_git("diff", f"--unified={unified}", base_sha, head_sha, "--", file)
40
+ if not file:
41
+ logger.warning(f"Skipping git diff for empty filename (base={base_sha}, head={head_sha})")
42
+ return ""
43
+
44
+ logger.debug(f"Generating diff for {file} between {base_sha}..{head_sha}")
45
+ output = self.run_git("diff", f"--unified={unified}", base_sha, head_sha, "--", file)
46
+ if not output.strip():
47
+ logger.info(f"No diff found for {file} (possibly deleted or not tracked)")
48
+
49
+ return output
26
50
 
27
51
  def get_changed_files(self, base_sha: str, head_sha: str) -> list[str]:
28
52
  output = self.run_git("diff", "--name-only", base_sha, head_sha)
29
- return [line.strip() for line in output.splitlines() if line.strip()]
53
+ files = [line.strip() for line in output.splitlines() if line.strip()]
54
+ logger.debug(f"Changed files between {base_sha}..{head_sha}: {files}")
55
+ return files
30
56
 
31
57
  def get_file_at_commit(self, file_path: str, sha: str) -> str | None:
58
+ if not file_path:
59
+ logger.warning(f"Skipping git show for empty file_path at {sha}")
60
+ return None
61
+
32
62
  try:
33
63
  return self.run_git("show", f"{sha}:{file_path}")
34
- except subprocess.CalledProcessError:
64
+ except subprocess.CalledProcessError as e:
65
+ logger.warning(f"File '{file_path}' not found in commit {sha}: {e.stderr.strip()}")
35
66
  return None
@@ -0,0 +1,42 @@
1
+ from ai_review.libs.asynchronous.gather import bounded_gather
2
+ from ai_review.libs.logger import get_logger
3
+ from ai_review.services.hook import hook
4
+ from ai_review.services.review.gateway.review_comment_gateway import ReviewCommentGateway
5
+ from ai_review.services.review.internal.inline.schema import InlineCommentListSchema, InlineCommentSchema
6
+ from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
7
+ from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
8
+ from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
9
+ from ai_review.services.vcs.types import VCSClientProtocol
10
+
11
+ logger = get_logger("REVIEW_DRY_RUN_COMMENT_GATEWAY")
12
+
13
+
14
+ class ReviewDryRunCommentGateway(ReviewCommentGateway):
15
+ def __init__(self, vcs: VCSClientProtocol):
16
+ super().__init__(vcs)
17
+ logger.warning("Running in DRY RUN mode — no comments will be posted to VCS")
18
+
19
+ async def process_inline_reply(self, thread_id: str, reply: InlineCommentReplySchema) -> None:
20
+ await hook.emit_inline_comment_reply_start(reply)
21
+ logger.info(f"[dry-run] Would create inline reply for thread {thread_id}:\n{reply.body_with_tag}")
22
+ await hook.emit_inline_comment_reply_complete(reply)
23
+
24
+ async def process_summary_reply(self, thread_id: str, reply: SummaryCommentReplySchema) -> None:
25
+ await hook.emit_summary_comment_reply_start(reply)
26
+ logger.info(f"[dry-run] Would create summary reply for thread {thread_id}:\n{reply.body_with_tag}")
27
+ await hook.emit_summary_comment_reply_complete(reply)
28
+
29
+ async def process_inline_comment(self, comment: InlineCommentSchema) -> None:
30
+ await hook.emit_inline_comment_start(comment)
31
+ logger.info(
32
+ f"[dry-run] Would create inline comment for {comment.file}:{comment.line}:\n{comment.body_with_tag}"
33
+ )
34
+ await hook.emit_inline_comment_complete(comment)
35
+
36
+ async def process_summary_comment(self, comment: SummaryCommentSchema) -> None:
37
+ await hook.emit_summary_comment_start(comment)
38
+ logger.info(f"[dry-run] Would create summary comment:\n{comment.body_with_tag}")
39
+ await hook.emit_summary_comment_complete(comment)
40
+
41
+ async def process_inline_comments(self, comments: InlineCommentListSchema) -> None:
42
+ await bounded_gather([self.process_inline_comment(comment) for comment in comments.root])
@@ -1,3 +1,4 @@
1
+ from ai_review.config import settings
1
2
  from ai_review.libs.logger import get_logger
2
3
  from ai_review.services.artifacts.service import ArtifactsService
3
4
  from ai_review.services.cost.service import CostService
@@ -5,8 +6,9 @@ from ai_review.services.diff.service import DiffService
5
6
  from ai_review.services.git.service import GitService
6
7
  from ai_review.services.llm.factory import get_llm_client
7
8
  from ai_review.services.prompt.service import PromptService
8
- from ai_review.services.review.gateway.comment import ReviewCommentGateway
9
- from ai_review.services.review.gateway.llm import ReviewLLMGateway
9
+ from ai_review.services.review.gateway.review_comment_gateway import ReviewCommentGateway
10
+ from ai_review.services.review.gateway.review_dry_run_comment_gateway import ReviewDryRunCommentGateway
11
+ from ai_review.services.review.gateway.review_llm_gateway import ReviewLLMGateway
10
12
  from ai_review.services.review.internal.inline.service import InlineCommentService
11
13
  from ai_review.services.review.internal.inline_reply.service import InlineCommentReplyService
12
14
  from ai_review.services.review.internal.policy.service import ReviewPolicyService
@@ -42,7 +44,11 @@ class ReviewService:
42
44
  cost=self.cost,
43
45
  artifacts=self.artifacts
44
46
  )
45
- self.review_comment_gateway = ReviewCommentGateway(vcs=self.vcs)
47
+ self.review_comment_gateway = (
48
+ ReviewDryRunCommentGateway(vcs=self.vcs)
49
+ if settings.review.dry_run
50
+ else ReviewCommentGateway(vcs=self.vcs)
51
+ )
46
52
 
47
53
  self.inline_review_runner = InlineReviewRunner(
48
54
  vcs=self.vcs,
@@ -13,11 +13,14 @@ def get_review_comment_from_bitbucket_pr_comment(comment: BitbucketPRCommentSche
13
13
  username=user.nickname if user else "",
14
14
  )
15
15
 
16
+ file = comment.inline.path if comment.inline and comment.inline.path else None
17
+ line = comment.inline.to_line if comment.inline else None
18
+
16
19
  return ReviewCommentSchema(
17
20
  id=comment.id,
18
21
  body=comment.content.raw or "",
19
- file=comment.inline.path if comment.inline else None,
20
- line=comment.inline.to_line if comment.inline else None,
22
+ file=file,
23
+ line=line,
21
24
  author=author,
22
25
  parent_id=parent_id,
23
26
  thread_id=thread_id,
@@ -207,22 +207,33 @@ class BitbucketVCSClient(VCSClientProtocol):
207
207
  try:
208
208
  comments = await self.get_inline_comments()
209
209
 
210
- threads: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
210
+ threads_by_id: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
211
211
  for comment in comments:
212
- threads[comment.thread_id].append(comment)
212
+ if not comment.file:
213
+ continue
213
214
 
214
- logger.info(f"Built {len(threads)} inline threads for {self.pull_request_ref}")
215
+ threads_by_id[comment.thread_id].append(comment)
215
216
 
216
- return [
217
- ReviewThreadSchema(
218
- id=thread_id,
219
- kind=ThreadKind.INLINE,
220
- file=thread[0].file,
221
- line=thread[0].line,
222
- comments=sorted(thread, key=lambda c: int(c.id)),
217
+ logger.info(f"Built {len(threads_by_id)} inline threads for {self.pull_request_ref}")
218
+
219
+ threads: list[ReviewThreadSchema] = []
220
+ for thread_id, thread in threads_by_id.items():
221
+ file = thread[0].file
222
+ line = thread[0].line
223
+ if not file:
224
+ continue
225
+
226
+ threads.append(
227
+ ReviewThreadSchema(
228
+ id=thread_id,
229
+ kind=ThreadKind.INLINE,
230
+ file=file,
231
+ line=line,
232
+ comments=sorted(thread, key=lambda t: int(t.id)),
233
+ )
223
234
  )
224
- for thread_id, thread in threads.items()
225
- ]
235
+
236
+ return threads
226
237
  except Exception as error:
227
238
  logger.exception(f"Failed to fetch inline threads for {self.pull_request_ref}: {error}")
228
239
  return []
@@ -16,11 +16,13 @@ def get_review_comment_from_gitlab_note(
16
16
  note: GitLabNoteSchema,
17
17
  discussion: GitLabDiscussionSchema
18
18
  ) -> ReviewCommentSchema:
19
+ position = note.position or discussion.position
20
+
19
21
  return ReviewCommentSchema(
20
22
  id=note.id,
21
23
  body=note.body or "",
22
- file=discussion.position.new_path if discussion.position else None,
23
- line=discussion.position.new_line if discussion.position else None,
24
+ file=position.new_path if position else None,
25
+ line=position.new_line if position else None,
24
26
  author=get_user_from_gitlab_user(note.author),
25
27
  thread_id=discussion.id,
26
28
  )
@@ -1,8 +1,6 @@
1
1
  from ai_review.clients.gitlab.client import get_gitlab_http_client
2
- from ai_review.clients.gitlab.mr.schema.discussions import (
3
- GitLabDiscussionPositionSchema,
4
- GitLabCreateMRDiscussionRequestSchema,
5
- )
2
+ from ai_review.clients.gitlab.mr.schema.discussions import GitLabCreateMRDiscussionRequestSchema
3
+ from ai_review.clients.gitlab.mr.schema.position import GitLabPositionSchema
6
4
  from ai_review.config import settings
7
5
  from ai_review.libs.logger import get_logger
8
6
  from ai_review.services.vcs.gitlab.adapter import get_user_from_gitlab_user, get_review_comment_from_gitlab_note
@@ -135,7 +133,7 @@ class GitLabVCSClient(VCSClientProtocol):
135
133
 
136
134
  request = GitLabCreateMRDiscussionRequestSchema(
137
135
  body=message,
138
- position=GitLabDiscussionPositionSchema(
136
+ position=GitLabPositionSchema(
139
137
  position_type="text",
140
138
  base_sha=response.diff_refs.base_sha,
141
139
  head_sha=response.diff_refs.head_sha,
@@ -191,19 +189,28 @@ class GitLabVCSClient(VCSClientProtocol):
191
189
  )
192
190
  logger.info(f"Fetched inline threads for MR {self.merge_request_ref}")
193
191
 
194
- threads = [
195
- ReviewThreadSchema(
196
- id=discussion.id,
197
- kind=ThreadKind.INLINE,
198
- file=discussion.position.new_path if discussion.position else None,
199
- line=discussion.position.new_line if discussion.position else None,
200
- comments=[
201
- get_review_comment_from_gitlab_note(note, discussion)
202
- for note in discussion.notes
203
- ]
192
+ threads: list[ReviewThreadSchema] = []
193
+ for discussion in response.root:
194
+ if not discussion.notes:
195
+ continue
196
+
197
+ position = discussion.position or (
198
+ discussion.notes[0].position if discussion.notes else None
204
199
  )
205
- for discussion in response.root if discussion.notes
206
- ]
200
+
201
+ threads.append(
202
+ ReviewThreadSchema(
203
+ id=discussion.id,
204
+ kind=ThreadKind.INLINE,
205
+ file=position.new_path if position else None,
206
+ line=position.new_line if position else None,
207
+ comments=[
208
+ get_review_comment_from_gitlab_note(note, discussion)
209
+ for note in discussion.notes
210
+ ],
211
+ )
212
+ )
213
+
207
214
  logger.info(f"Built {len(threads)} inline threads for MR {self.merge_request_ref}")
208
215
  return threads
209
216
  except Exception as error:
@@ -12,13 +12,14 @@ from ai_review.clients.gitlab.mr.schema.discussions import (
12
12
  GitLabGetMRDiscussionsResponseSchema,
13
13
  GitLabCreateMRDiscussionRequestSchema,
14
14
  GitLabCreateMRDiscussionResponseSchema,
15
- GitLabCreateMRDiscussionReplyResponseSchema, GitLabDiscussionPositionSchema,
15
+ GitLabCreateMRDiscussionReplyResponseSchema,
16
16
  )
17
17
  from ai_review.clients.gitlab.mr.schema.notes import (
18
18
  GitLabNoteSchema,
19
19
  GitLabGetMRNotesResponseSchema,
20
20
  GitLabCreateMRNoteResponseSchema,
21
21
  )
22
+ from ai_review.clients.gitlab.mr.schema.position import GitLabPositionSchema
22
23
  from ai_review.clients.gitlab.mr.types import GitLabMergeRequestsHTTPClientProtocol
23
24
  from ai_review.config import settings
24
25
  from ai_review.libs.config.vcs.base import GitLabVCSConfig
@@ -81,17 +82,42 @@ class FakeGitLabMergeRequestsHTTPClient(GitLabMergeRequestsHTTPClientProtocol):
81
82
  GitLabDiscussionSchema(
82
83
  id="discussion-1",
83
84
  notes=[
84
- GitLabNoteSchema(id=10, body="Inline comment A"),
85
- GitLabNoteSchema(id=11, body="Inline comment B"),
85
+ GitLabNoteSchema(
86
+ id=10,
87
+ body="Inline comment A",
88
+ position=GitLabPositionSchema(
89
+ base_sha="abc123",
90
+ head_sha="def456",
91
+ start_sha="ghi789",
92
+ new_path="src/app.py",
93
+ new_line=12,
94
+ ),
95
+ ),
96
+ GitLabNoteSchema(
97
+ id=11,
98
+ body="Inline comment B",
99
+ position=GitLabPositionSchema(
100
+ base_sha="abc123",
101
+ head_sha="def456",
102
+ start_sha="ghi789",
103
+ new_path="src/app.py",
104
+ new_line=14,
105
+ ),
106
+ ),
86
107
  ],
87
- position=GitLabDiscussionPositionSchema(
108
+ position=GitLabPositionSchema(
88
109
  base_sha="abc123",
89
110
  head_sha="def456",
90
111
  start_sha="ghi789",
91
112
  new_path="src/app.py",
92
113
  new_line=12,
93
114
  ),
94
- )
115
+ ),
116
+ GitLabDiscussionSchema(
117
+ id="discussion-2",
118
+ notes=[GitLabNoteSchema(id=20, body="Outdated diff comment", position=None)],
119
+ position=None,
120
+ ),
95
121
  ]
96
122
  )
97
123
 
@@ -2,7 +2,7 @@ from typing import Any
2
2
 
3
3
  import pytest
4
4
 
5
- from ai_review.services.review.gateway.comment import ReviewCommentGateway
5
+ from ai_review.services.review.gateway.review_comment_gateway import ReviewCommentGateway
6
6
  from ai_review.services.review.gateway.types import ReviewCommentGatewayProtocol
7
7
  from ai_review.services.review.internal.inline.schema import InlineCommentSchema, InlineCommentListSchema
8
8
  from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
@@ -0,0 +1,103 @@
1
+ from typing import Any
2
+
3
+ import pytest
4
+
5
+ from ai_review.services.review.gateway.review_dry_run_comment_gateway import ReviewDryRunCommentGateway
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 FakeReviewDryRunCommentGateway(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=10,
31
+ comments=[
32
+ ReviewCommentSchema(
33
+ id="c1",
34
+ body="#ai-review-inline some comment",
35
+ file="file.py",
36
+ line=10,
37
+ author=fake_user,
38
+ ),
39
+ ],
40
+ )
41
+
42
+ fake_summary_thread = ReviewThreadSchema(
43
+ id="t2",
44
+ kind=ThreadKind.SUMMARY,
45
+ comments=[
46
+ ReviewCommentSchema(
47
+ id="c2",
48
+ body="#ai-review-summary summary comment",
49
+ author=fake_user,
50
+ ),
51
+ ],
52
+ )
53
+
54
+ self.responses = responses or {
55
+ "get_inline_threads": [fake_inline_thread],
56
+ "get_summary_threads": [fake_summary_thread],
57
+ "has_existing_inline_comments": False,
58
+ "has_existing_summary_comments": False,
59
+ }
60
+
61
+ # --- Методы чтения ---
62
+ async def get_inline_threads(self) -> list[ReviewThreadSchema]:
63
+ self.calls.append(("get_inline_threads", {}))
64
+ return self.responses["get_inline_threads"]
65
+
66
+ async def get_summary_threads(self) -> list[ReviewThreadSchema]:
67
+ self.calls.append(("get_summary_threads", {}))
68
+ return self.responses["get_summary_threads"]
69
+
70
+ async def has_existing_inline_comments(self) -> bool:
71
+ self.calls.append(("has_existing_inline_comments", {}))
72
+ return self.responses["has_existing_inline_comments"]
73
+
74
+ async def has_existing_summary_comments(self) -> bool:
75
+ self.calls.append(("has_existing_summary_comments", {}))
76
+ return self.responses["has_existing_summary_comments"]
77
+
78
+ async def process_inline_reply(self, thread_id: str, reply: InlineCommentReplySchema) -> None:
79
+ self.calls.append(("process_inline_reply", {"thread_id": thread_id, "reply": reply}))
80
+
81
+ async def process_summary_reply(self, thread_id: str, reply: SummaryCommentReplySchema) -> None:
82
+ self.calls.append(("process_summary_reply", {"thread_id": thread_id, "reply": reply}))
83
+
84
+ async def process_inline_comment(self, comment: InlineCommentSchema) -> None:
85
+ self.calls.append(("process_inline_comment", {"comment": comment}))
86
+
87
+ async def process_summary_comment(self, comment: SummaryCommentSchema) -> None:
88
+ self.calls.append(("process_summary_comment", {"comment": comment}))
89
+
90
+ async def process_inline_comments(self, comments: InlineCommentListSchema) -> None:
91
+ self.calls.append(("process_inline_comments", {"comments": comments}))
92
+ for comment in comments.root:
93
+ await self.process_inline_comment(comment)
94
+
95
+
96
+ @pytest.fixture
97
+ def fake_review_dry_run_comment_gateway() -> FakeReviewDryRunCommentGateway:
98
+ return FakeReviewDryRunCommentGateway()
99
+
100
+
101
+ @pytest.fixture
102
+ def review_dry_run_comment_gateway(fake_vcs_client: VCSClientProtocol) -> ReviewDryRunCommentGateway:
103
+ return ReviewDryRunCommentGateway(vcs=fake_vcs_client)
@@ -0,0 +1,34 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.gateway.review_llm_gateway import ReviewLLMGateway
4
+ from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol
5
+ from ai_review.tests.fixtures.services.artifacts import FakeArtifactsService
6
+ from ai_review.tests.fixtures.services.cost import FakeCostService
7
+ from ai_review.tests.fixtures.services.llm import FakeLLMClient
8
+
9
+
10
+ class FakeReviewLLMGateway(ReviewLLMGatewayProtocol):
11
+ def __init__(self):
12
+ self.calls: list[tuple[str, dict]] = []
13
+
14
+ async def ask(self, prompt: str, prompt_system: str) -> str:
15
+ self.calls.append(("ask", {"prompt": prompt, "prompt_system": prompt_system}))
16
+ return "FAKE_LLM_RESPONSE"
17
+
18
+
19
+ @pytest.fixture
20
+ def fake_review_llm_gateway() -> FakeReviewLLMGateway:
21
+ return FakeReviewLLMGateway()
22
+
23
+
24
+ @pytest.fixture
25
+ def review_llm_gateway(
26
+ fake_llm_client: FakeLLMClient,
27
+ fake_cost_service: FakeCostService,
28
+ fake_artifacts_service: FakeArtifactsService,
29
+ ) -> ReviewLLMGateway:
30
+ return ReviewLLMGateway(
31
+ llm=fake_llm_client,
32
+ cost=fake_cost_service,
33
+ artifacts=fake_artifacts_service,
34
+ )
@@ -1,7 +1,7 @@
1
1
  import pytest
2
2
 
3
3
  from ai_review.config import settings
4
- from ai_review.services.review.gateway.comment import ReviewCommentGateway
4
+ from ai_review.services.review.gateway.review_comment_gateway import ReviewCommentGateway
5
5
  from ai_review.services.review.internal.inline.schema import InlineCommentSchema, InlineCommentListSchema
6
6
  from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
7
7
  from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
@@ -0,0 +1,93 @@
1
+ import pytest
2
+
3
+ from ai_review.services.review.gateway.review_dry_run_comment_gateway import ReviewDryRunCommentGateway
4
+ from ai_review.services.review.internal.inline.schema import InlineCommentSchema, InlineCommentListSchema
5
+ from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
6
+ from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
7
+ from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
8
+ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
9
+
10
+
11
+ @pytest.mark.asyncio
12
+ async def test_process_inline_reply_dry_run_logs_and_no_vcs_calls(
13
+ capsys,
14
+ fake_vcs_client: FakeVCSClient,
15
+ review_dry_run_comment_gateway: ReviewDryRunCommentGateway
16
+ ):
17
+ """Dry-run: should log the inline reply but not call VCS."""
18
+ reply = InlineCommentReplySchema(message="AI reply dry-run")
19
+ await review_dry_run_comment_gateway.process_inline_reply("t1", reply)
20
+ output = capsys.readouterr().out
21
+
22
+ assert "[dry-run]" in output
23
+ assert "Would create inline reply" in output
24
+ assert not any(call[0].startswith("create_") for call in fake_vcs_client.calls)
25
+
26
+
27
+ @pytest.mark.asyncio
28
+ async def test_process_summary_reply_dry_run_logs_and_no_vcs_calls(
29
+ capsys,
30
+ fake_vcs_client: FakeVCSClient,
31
+ review_dry_run_comment_gateway: ReviewDryRunCommentGateway
32
+ ):
33
+ """Dry-run: should log the summary reply but not call VCS."""
34
+ reply = SummaryCommentReplySchema(text="Dry-run summary reply")
35
+ await review_dry_run_comment_gateway.process_summary_reply("t2", reply)
36
+ output = capsys.readouterr().out
37
+
38
+ assert "[dry-run]" in output
39
+ assert "Would create summary reply" in output
40
+ assert not any(call[0].startswith("create_") for call in fake_vcs_client.calls)
41
+
42
+
43
+ @pytest.mark.asyncio
44
+ async def test_process_inline_comment_dry_run_logs_and_no_vcs_calls(
45
+ capsys,
46
+ fake_vcs_client: FakeVCSClient,
47
+ review_dry_run_comment_gateway: ReviewDryRunCommentGateway
48
+ ):
49
+ """Dry-run: should log inline comment without creating one."""
50
+ comment = InlineCommentSchema(file="a.py", line=10, message="Test comment")
51
+ await review_dry_run_comment_gateway.process_inline_comment(comment)
52
+ output = capsys.readouterr().out
53
+
54
+ assert "[dry-run]" in output
55
+ assert "Would create inline comment" in output
56
+ assert "a.py" in output
57
+ assert not any(call[0].startswith("create_") for call in fake_vcs_client.calls)
58
+
59
+
60
+ @pytest.mark.asyncio
61
+ async def test_process_summary_comment_dry_run_logs_and_no_vcs_calls(
62
+ capsys,
63
+ fake_vcs_client: FakeVCSClient,
64
+ review_dry_run_comment_gateway: ReviewDryRunCommentGateway
65
+ ):
66
+ """Dry-run: should log summary comment but not send it."""
67
+ comment = SummaryCommentSchema(text="Dry-run summary comment")
68
+ await review_dry_run_comment_gateway.process_summary_comment(comment)
69
+ output = capsys.readouterr().out
70
+
71
+ assert "[dry-run]" in output
72
+ assert "Would create summary comment" in output
73
+ assert not any(call[0].startswith("create_") for call in fake_vcs_client.calls)
74
+
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_process_inline_comments_iterates_all(
78
+ capsys,
79
+ fake_vcs_client: FakeVCSClient,
80
+ review_dry_run_comment_gateway: ReviewDryRunCommentGateway
81
+ ):
82
+ """Dry-run: should iterate through all inline comments and log each."""
83
+ comments = InlineCommentListSchema(root=[
84
+ InlineCommentSchema(file="a.py", line=1, message="C1"),
85
+ InlineCommentSchema(file="b.py", line=2, message="C2"),
86
+ ])
87
+ await review_dry_run_comment_gateway.process_inline_comments(comments)
88
+ output = capsys.readouterr().out
89
+
90
+ assert "[dry-run]" in output
91
+ assert "a.py" in output
92
+ assert "b.py" in output
93
+ assert not any(call[0].startswith("create_") for call in fake_vcs_client.calls)
@@ -1,26 +1,12 @@
1
1
  import pytest
2
2
 
3
3
  from ai_review.services.llm.types import ChatResultSchema
4
- from ai_review.services.review.gateway.llm import ReviewLLMGateway
4
+ from ai_review.services.review.gateway.review_llm_gateway import ReviewLLMGateway
5
5
  from ai_review.tests.fixtures.services.artifacts import FakeArtifactsService
6
6
  from ai_review.tests.fixtures.services.cost import FakeCostService
7
7
  from ai_review.tests.fixtures.services.llm import FakeLLMClient
8
8
 
9
9
 
10
- @pytest.fixture
11
- def review_llm_gateway(
12
- fake_llm_client: FakeLLMClient,
13
- fake_cost_service: FakeCostService,
14
- fake_artifacts_service: FakeArtifactsService,
15
- ) -> ReviewLLMGateway:
16
- """Fixture providing ReviewLLMGateway with fake dependencies."""
17
- return ReviewLLMGateway(
18
- llm=fake_llm_client,
19
- cost=fake_cost_service,
20
- artifacts=fake_artifacts_service,
21
- )
22
-
23
-
24
10
  @pytest.mark.asyncio
25
11
  async def test_ask_happy_path(
26
12
  review_llm_gateway: ReviewLLMGateway,
@@ -4,8 +4,8 @@ from ai_review.services.review.runner.context import ContextReviewRunner
4
4
  from ai_review.tests.fixtures.services.cost import FakeCostService
5
5
  from ai_review.tests.fixtures.services.diff import FakeDiffService
6
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
7
+ from ai_review.tests.fixtures.services.review.gateway.review_comment_gateway import FakeReviewCommentGateway
8
+ from ai_review.tests.fixtures.services.review.gateway.review_llm_gateway import FakeReviewLLMGateway
9
9
  from ai_review.tests.fixtures.services.review.internal.inline import FakeInlineCommentService
10
10
  from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
11
11
  from ai_review.tests.fixtures.services.vcs import FakeVCSClient
@@ -6,8 +6,8 @@ from ai_review.tests.fixtures.services.cost import FakeCostService
6
6
  from ai_review.tests.fixtures.services.diff import FakeDiffService
7
7
  from ai_review.tests.fixtures.services.git import FakeGitService
8
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
9
+ from ai_review.tests.fixtures.services.review.gateway.review_comment_gateway import FakeReviewCommentGateway
10
+ from ai_review.tests.fixtures.services.review.gateway.review_llm_gateway import FakeReviewLLMGateway
11
11
  from ai_review.tests.fixtures.services.review.internal.inline import FakeInlineCommentService
12
12
  from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
13
13
  from ai_review.tests.fixtures.services.vcs import FakeVCSClient
@@ -6,8 +6,8 @@ from ai_review.tests.fixtures.services.cost import FakeCostService
6
6
  from ai_review.tests.fixtures.services.diff import FakeDiffService
7
7
  from ai_review.tests.fixtures.services.git import FakeGitService
8
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
9
+ from ai_review.tests.fixtures.services.review.gateway.review_comment_gateway import FakeReviewCommentGateway
10
+ from ai_review.tests.fixtures.services.review.gateway.review_llm_gateway import FakeReviewLLMGateway
11
11
  from ai_review.tests.fixtures.services.review.internal.inline_reply import FakeInlineCommentReplyService
12
12
  from ai_review.tests.fixtures.services.vcs import FakeVCSClient
13
13
 
@@ -5,8 +5,8 @@ from ai_review.services.review.runner.summary import SummaryReviewRunner
5
5
  from ai_review.tests.fixtures.services.cost import FakeCostService
6
6
  from ai_review.tests.fixtures.services.diff import FakeDiffService
7
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
8
+ from ai_review.tests.fixtures.services.review.gateway.review_comment_gateway import FakeReviewCommentGateway
9
+ from ai_review.tests.fixtures.services.review.gateway.review_llm_gateway import FakeReviewLLMGateway
10
10
  from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
11
11
  from ai_review.tests.fixtures.services.review.internal.summary import FakeSummaryCommentService
12
12
  from ai_review.tests.fixtures.services.vcs import FakeVCSClient
@@ -5,8 +5,8 @@ from ai_review.services.vcs.types import ReviewInfoSchema, ReviewThreadSchema, R
5
5
  from ai_review.tests.fixtures.services.cost import FakeCostService
6
6
  from ai_review.tests.fixtures.services.diff import FakeDiffService
7
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
8
+ from ai_review.tests.fixtures.services.review.gateway.review_comment_gateway import FakeReviewCommentGateway
9
+ from ai_review.tests.fixtures.services.review.gateway.review_llm_gateway import FakeReviewLLMGateway
10
10
  from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
11
11
  from ai_review.tests.fixtures.services.review.internal.summary_reply import FakeSummaryCommentReplyService
12
12
  from ai_review.tests.fixtures.services.vcs import FakeVCSClient
@@ -1,6 +1,8 @@
1
1
  import pytest
2
2
 
3
3
  from ai_review.services.llm.types import ChatResultSchema
4
+ from ai_review.services.review.gateway.review_comment_gateway import ReviewCommentGateway
5
+ from ai_review.services.review.gateway.review_dry_run_comment_gateway import ReviewDryRunCommentGateway
4
6
  from ai_review.services.review.service import ReviewService
5
7
  from ai_review.tests.fixtures.services.cost import FakeCostService
6
8
  from ai_review.tests.fixtures.services.review.runner.context import FakeContextReviewRunner
@@ -91,3 +93,19 @@ def test_report_total_cost_no_data(capsys, review_service: ReviewService):
91
93
  output = capsys.readouterr().out
92
94
 
93
95
  assert "No cost data collected" in output
96
+
97
+
98
+ def test_review_service_uses_dry_run_comment_gateway(monkeypatch: pytest.MonkeyPatch):
99
+ """Should use ReviewDryRunCommentGateway when settings.review.dry_run=True."""
100
+ monkeypatch.setattr("ai_review.config.settings.review.dry_run", True)
101
+
102
+ service = ReviewService()
103
+ assert type(service.review_comment_gateway) is ReviewDryRunCommentGateway
104
+
105
+
106
+ def test_review_service_uses_real_comment_gateway(monkeypatch: pytest.MonkeyPatch):
107
+ """Should use normal ReviewCommentGateway when dry_run=False."""
108
+ monkeypatch.setattr("ai_review.config.settings.review.dry_run", False)
109
+
110
+ service = ReviewService()
111
+ assert type(service.review_comment_gateway) is ReviewCommentGateway
@@ -1,8 +1,6 @@
1
- from ai_review.clients.gitlab.mr.schema.discussions import (
2
- GitLabDiscussionSchema,
3
- GitLabDiscussionPositionSchema,
4
- )
1
+ from ai_review.clients.gitlab.mr.schema.discussions import GitLabDiscussionSchema
5
2
  from ai_review.clients.gitlab.mr.schema.notes import GitLabNoteSchema
3
+ from ai_review.clients.gitlab.mr.schema.position import GitLabPositionSchema
6
4
  from ai_review.clients.gitlab.mr.schema.user import GitLabUserSchema
7
5
  from ai_review.services.vcs.gitlab.adapter import get_review_comment_from_gitlab_note
8
6
  from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
@@ -18,7 +16,7 @@ def test_maps_all_fields_correctly():
18
16
  discussion = GitLabDiscussionSchema(
19
17
  id="42",
20
18
  notes=[note],
21
- position=GitLabDiscussionPositionSchema(
19
+ position=GitLabPositionSchema(
22
20
  base_sha="AAA000",
23
21
  head_sha="BBB111",
24
22
  start_sha="CCC222",
@@ -48,7 +46,7 @@ def test_maps_with_missing_author():
48
46
  discussion = GitLabDiscussionSchema(
49
47
  id="7",
50
48
  notes=[note],
51
- position=GitLabDiscussionPositionSchema(
49
+ position=GitLabPositionSchema(
52
50
  base_sha="AAA000",
53
51
  head_sha="BBB111",
54
52
  start_sha="CCC222",
@@ -103,3 +101,34 @@ def test_maps_with_empty_body_and_defaults():
103
101
  assert result.line is None
104
102
  assert result.thread_id == "100"
105
103
  assert isinstance(result.author, UserSchema)
104
+
105
+
106
+ def test_maps_with_note_position_fallback():
107
+ """Should use note.position when discussion.position is missing."""
108
+ note = GitLabNoteSchema(
109
+ id=77,
110
+ body="Inline note with its own position",
111
+ author=GitLabUserSchema(id=2, name="Carol", username="carol"),
112
+ position=GitLabPositionSchema(
113
+ base_sha="aaa",
114
+ head_sha="bbb",
115
+ start_sha="ccc",
116
+ new_path="module/utils.py",
117
+ new_line=42,
118
+ ),
119
+ )
120
+ discussion = GitLabDiscussionSchema(
121
+ id="200",
122
+ notes=[note],
123
+ position=None,
124
+ )
125
+
126
+ result = get_review_comment_from_gitlab_note(note, discussion)
127
+
128
+ assert isinstance(result, ReviewCommentSchema)
129
+ assert result.id == 77
130
+ assert result.file == "module/utils.py"
131
+ assert result.line == 42
132
+ assert result.thread_id == "200"
133
+ assert result.body == "Inline note with its own position"
134
+ assert result.author.name == "Carol"
@@ -71,14 +71,20 @@ async def test_get_inline_comments_returns_expected_list(
71
71
  gitlab_vcs_client: GitLabVCSClient,
72
72
  fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient,
73
73
  ):
74
- """Should return inline comments from MR discussions."""
74
+ """Should return inline comments from MR discussions (including ones without position)."""
75
75
  comments = await gitlab_vcs_client.get_inline_comments()
76
76
 
77
77
  assert all(isinstance(c, ReviewCommentSchema) for c in comments)
78
- assert len(comments) == 2
78
+ assert len(comments) == 3
79
79
 
80
80
  first = comments[0]
81
81
  assert first.body == "Inline comment A"
82
+ assert first.file == "src/app.py"
83
+ assert first.line == 12
84
+
85
+ last = comments[-1]
86
+ assert last.file is None
87
+ assert last.line is None
82
88
 
83
89
  called_methods = [name for name, _ in fake_gitlab_merge_requests_http_client.calls]
84
90
  assert called_methods == ["get_discussions"]
@@ -183,19 +189,25 @@ async def test_get_inline_threads_returns_valid_schema(
183
189
  gitlab_vcs_client: GitLabVCSClient,
184
190
  fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient,
185
191
  ):
186
- """Should transform GitLab discussions into inline threads with proper fields."""
192
+ """Should transform GitLab discussions into inline threads, including those without position."""
187
193
  threads = await gitlab_vcs_client.get_inline_threads()
188
194
 
189
- assert all(isinstance(t, ReviewThreadSchema) for t in threads)
190
- assert len(threads) == 1
195
+ assert all(isinstance(thread, ReviewThreadSchema) for thread in threads)
196
+ assert len(threads) == 2
191
197
 
192
- thread = threads[0]
193
- assert thread.id == "discussion-1"
194
- assert thread.kind == ThreadKind.INLINE
195
- assert thread.file == "src/app.py"
196
- assert thread.line == 12
197
- assert len(thread.comments) == 2
198
- assert isinstance(thread.comments[0], ReviewCommentSchema)
198
+ first_thread = threads[0]
199
+ assert first_thread.id == "discussion-1"
200
+ assert first_thread.kind == ThreadKind.INLINE
201
+ assert first_thread.file == "src/app.py"
202
+ assert first_thread.line == 12
203
+ assert len(first_thread.comments) == 2
204
+ assert isinstance(first_thread.comments[0], ReviewCommentSchema)
205
+
206
+ second_thread = threads[1]
207
+ assert second_thread.id == "discussion-2"
208
+ assert second_thread.file is None
209
+ assert second_thread.line is None
210
+ assert len(second_thread.comments) == 1
199
211
 
200
212
  called = [name for name, _ in fake_gitlab_merge_requests_http_client.calls]
201
213
  assert "get_discussions" in called
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xai-review
3
- Version: 0.28.0
3
+ Version: 0.30.0
4
4
  Summary: AI-powered code review tool
5
5
  Author-email: Nikita Filonov <nikita.filonov@example.com>
6
6
  Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
@@ -211,7 +211,7 @@ jobs:
211
211
  runs-on: ubuntu-latest
212
212
  steps:
213
213
  - uses: actions/checkout@v4
214
- - uses: Nikita-Filonov/ai-review@v0.28.0
214
+ - uses: Nikita-Filonov/ai-review@v0.30.0
215
215
  with:
216
216
  review-command: ${{ inputs.review-command }}
217
217
  env:
@@ -49,8 +49,9 @@ ai_review/clients/gitlab/mr/client.py,sha256=ydkecV9SlmXJvkoBVF0c-5C7RsadYTP9RXC
49
49
  ai_review/clients/gitlab/mr/types.py,sha256=3PpUjnek6AOAiIC0tYRuX4RwB2UeoIrSxigTvsGC1iI,1563
50
50
  ai_review/clients/gitlab/mr/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  ai_review/clients/gitlab/mr/schema/changes.py,sha256=qYvAih3zSvNuF4C8PtnuwmZVFk8VHM14miPxgC8hWG4,824
52
- ai_review/clients/gitlab/mr/schema/discussions.py,sha256=mbVc-n39MjJcWsSHQCFsLTJuvDZb3QrM-CjWYBNf8us,1083
53
- ai_review/clients/gitlab/mr/schema/notes.py,sha256=Z0NxDbgEo9CpKZbBjJNpoEuv8Q_u0oiH5GAjBeS2KMo,561
52
+ ai_review/clients/gitlab/mr/schema/discussions.py,sha256=pZ6ajZaQQI7_jO83FFXQmR_xruzda_pVSv_V0Klvc2w,966
53
+ ai_review/clients/gitlab/mr/schema/notes.py,sha256=9wmwULegmTO6ETSjYlMC6Fc_DIeT_Wa6hiOB722ykUA,687
54
+ ai_review/clients/gitlab/mr/schema/position.py,sha256=oYml4x6rlrqGahEEbSB1c1ko70geL_0_otbwP0JqV6k,371
54
55
  ai_review/clients/gitlab/mr/schema/user.py,sha256=RxgCM8oryPBNPDaFxcVqe11MogchMGaO1gALkZiscrU,112
55
56
  ai_review/clients/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
57
  ai_review/clients/ollama/client.py,sha256=KoJ9J5_Vfpv5XNJREshE_gA46uo9J0Z3qVC7wJPEcX8,1720
@@ -65,7 +66,7 @@ ai_review/libs/json.py,sha256=g-P5_pNUomQ-bGHCXASvPKj9Og0s9MaLFVEAkzqGp1A,350
65
66
  ai_review/libs/logger.py,sha256=LbXR2Zk1btJ-83I-vHee7cUETgT1mHToSsqEI_8uM0U,370
66
67
  ai_review/libs/resources.py,sha256=s9taAbL1Shl_GiGkPpkkivUcM1Yh6d_IQAG97gffsJU,748
67
68
  ai_review/libs/asynchronous/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- ai_review/libs/asynchronous/gather.py,sha256=wH65sqBfrnwA1A9Juc5MSyLCJrcxzRqk2m0tSZ1tX8A,490
69
+ ai_review/libs/asynchronous/gather.py,sha256=ns7eFWeV6ukRM13v78ZGu6WEpojH6QoPS-V0KHdn-sk,735
69
70
  ai_review/libs/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
71
  ai_review/libs/config/artifacts.py,sha256=8BzbQu5GxwV6i6qzrUKM1De1Ogb00Ph5WTqwZ3fVpGg,483
71
72
  ai_review/libs/config/base.py,sha256=sPf3OKeF1ID0ouOwiVaUtvpWuZXJXQvIw5kbnPUyN9o,686
@@ -73,7 +74,7 @@ ai_review/libs/config/core.py,sha256=ZQ2QtYr7vAF0tXbVLvVwk9QFE5h6JjAKAUQWcb9gHws
73
74
  ai_review/libs/config/http.py,sha256=dx5PwgnGbPocUwf9QRhFmXmjfFDoeerOM04yB3B6S8w,398
74
75
  ai_review/libs/config/logger.py,sha256=oPmjpjf6EZwW7CgOjT8mOQdGnT98CLwXepiGB_ajZvU,384
75
76
  ai_review/libs/config/prompt.py,sha256=jPBYvi75u6BUIHOZnXCg5CiL0XRONDfW5T4WaYlxPEE,6066
76
- ai_review/libs/config/review.py,sha256=q2EwLvUzsOp_WW1gtd_8fvUhAoStWiNGicxOYP4gm9E,1198
77
+ ai_review/libs/config/review.py,sha256=jCfHfGfHs7Sc6H92hVUBd8bMRbLiIiThVmk6lFNlv40,1224
77
78
  ai_review/libs/config/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
79
  ai_review/libs/config/llm/base.py,sha256=ovvULFhfwH66_705D1O87ZGMeaQOZO7ZQhRUzzfzguU,2089
79
80
  ai_review/libs/config/llm/claude.py,sha256=MoalXkBA6pEp01znS8ohTRopfea9RUcqhZX5lOIuek8,293
@@ -137,7 +138,7 @@ ai_review/services/diff/service.py,sha256=yRb4e0fZcgFTGkAZKm5q8Gw4rWxc3nyFtpBw7a
137
138
  ai_review/services/diff/tools.py,sha256=YHmH6Ult_rucCd563UhG0geMzqrPhqKFZKyug79xNuA,1963
138
139
  ai_review/services/diff/types.py,sha256=uaX0hK_wfRG7Lxs0DnHgjDdkKGSQ1PS2-kZVCVjUeR8,700
139
140
  ai_review/services/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
- ai_review/services/git/service.py,sha256=_RMwgcllDoSLUzl84JML38fWkr7swnkUr6MJ46hSkWw,1282
141
+ ai_review/services/git/service.py,sha256=7kLCQqJfX--SsFcCWrNxblAEn_xLOYPUSZpr6NTlBUg,2570
141
142
  ai_review/services/git/types.py,sha256=QTOCTmR-Rt3sUjzZQHu2PGo_6un5gvNupifAa84wON4,413
142
143
  ai_review/services/hook/__init__.py,sha256=HDukG_ZosgGg4dT5GCGIzqZX7llbyYUofKVFeG7YR2A,98
143
144
  ai_review/services/hook/constants.py,sha256=GthpBviiqcb3Oj5uhiJhyYGmWuwKgLlToC8pyj8klRM,1585
@@ -161,10 +162,11 @@ ai_review/services/prompt/service.py,sha256=AvhLMfwmIxbnPWdViSSclwUUqnHGx2V-hSMs
161
162
  ai_review/services/prompt/tools.py,sha256=7yKQmHajlwFE37tvmMbbBYDmFObFVOb1ubHhNXBdAnE,1229
162
163
  ai_review/services/prompt/types.py,sha256=9p1RHuR5-5DNV0AtR_VtiT5OHo6eNqKzGggQoPWiSh8,1572
163
164
  ai_review/services/review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
164
- ai_review/services/review/service.py,sha256=oqRJfWcww-ZuW5BToXqEymLCLlQbRSZ1ng5LxpUG_Y8,5286
165
+ ai_review/services/review/service.py,sha256=aqt0dlA77_MPT4lcq1qbTjOZOKE-7y83I-7J5jwvro4,5579
165
166
  ai_review/services/review/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
- ai_review/services/review/gateway/comment.py,sha256=gOFoEFXIJ9SX7B05dTO605dUOnrchCtB-a2ExXCfUxE,5019
167
- ai_review/services/review/gateway/llm.py,sha256=5BQU8Vmo55pzWkVk6s_w5B3qX4tSkjjtj7wWWhLyc9s,1534
167
+ ai_review/services/review/gateway/review_comment_gateway.py,sha256=gOFoEFXIJ9SX7B05dTO605dUOnrchCtB-a2ExXCfUxE,5019
168
+ ai_review/services/review/gateway/review_dry_run_comment_gateway.py,sha256=3CLUBGt8ExX-r_d3PDUxhgaw-yUMqYYOvJbRzs-W63I,2420
169
+ ai_review/services/review/gateway/review_llm_gateway.py,sha256=5BQU8Vmo55pzWkVk6s_w5B3qX4tSkjjtj7wWWhLyc9s,1534
168
170
  ai_review/services/review/gateway/types.py,sha256=C9zOUF0ZDnSa568T3AIWIxpNdnpcB8QUxU1rVqx7lZc,1795
169
171
  ai_review/services/review/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
172
  ai_review/services/review/internal/inline/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -197,14 +199,14 @@ ai_review/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
197
199
  ai_review/services/vcs/factory.py,sha256=AfhpZjQ257BkLjb_7zUyw_EUnfEiCUHgTph7GGm-MY4,753
198
200
  ai_review/services/vcs/types.py,sha256=LemhQ4LAGlOdwMSF-HlYIo7taSRu4494YQ0Rp2PBgcg,3169
199
201
  ai_review/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
200
- ai_review/services/vcs/bitbucket/adapter.py,sha256=kWR7QXeg9GXnGyMpn_v-2roVRRFEAvGiKF8R-xXgczI,871
201
- ai_review/services/vcs/bitbucket/client.py,sha256=dhsgJ3uYzWEwEX1XEi_TTQkfCc5xAc9B_c1F9hwPR7M,10401
202
+ ai_review/services/vcs/bitbucket/adapter.py,sha256=b-8KT46C8WT-Sos-gUGFJsxIWY7mXfzTuJjIqYuzrBA,928
203
+ ai_review/services/vcs/bitbucket/client.py,sha256=MhqkFDewutX7DFBCFBDhDopFznwf92dAdZsxy_oS_mc,10726
202
204
  ai_review/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
203
205
  ai_review/services/vcs/github/adapter.py,sha256=pxcbSLEXkOg1c1NtzB0hkZJVvyC4An9pCzNK5sPJKbA,1212
204
206
  ai_review/services/vcs/github/client.py,sha256=rSVvpyovT1wDLq0fIQPe5UJqmgIgIFh2P-CPqIQ_sf0,9371
205
207
  ai_review/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
206
- ai_review/services/vcs/gitlab/adapter.py,sha256=RkBalrgY8EyxIB8QSH4r4PoTDhg46GROHh2zQyQ4zuY,999
207
- ai_review/services/vcs/gitlab/client.py,sha256=vPiIpTFEnLXkUV4tSqRJPab13kkLNlUfrIticzmTRGQ,9498
208
+ ai_review/services/vcs/gitlab/adapter.py,sha256=s6EK4Un5rucEJUN61hyzinrTCyvudziNrhK1AGK_XC0,1008
209
+ ai_review/services/vcs/gitlab/client.py,sha256=Dal_GeOAIdJzOQ2dRX7fm1E9p02no9_8sE5TnhIBo44,9779
208
210
  ai_review/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
209
211
  ai_review/tests/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
210
212
  ai_review/tests/fixtures/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -212,7 +214,7 @@ ai_review/tests/fixtures/clients/bitbucket.py,sha256=K4Ez_hTOVKz5-KlXbgTOVDec6ie
212
214
  ai_review/tests/fixtures/clients/claude.py,sha256=6ldJlSSea0zsZV0hRDMi9mqWm0hWT3mp_ROwG_sVU1c,2203
213
215
  ai_review/tests/fixtures/clients/gemini.py,sha256=zhLJhm49keKEBCPOf_pLu8_zCatsKKAWM4-gXOhaXeM,2429
214
216
  ai_review/tests/fixtures/clients/github.py,sha256=kC1L-nWZMn9O_uRfuT_B8R4sn8FRvISlBJMkRKaioS0,7814
215
- ai_review/tests/fixtures/clients/gitlab.py,sha256=9jVvo5b_-SULDAh7Kij7ueZqraqBZgBig0HuDzn8_tg,6921
217
+ ai_review/tests/fixtures/clients/gitlab.py,sha256=AD6NJOJSw76hjAEiWewQ6Vu5g-cfQn0GTtdchuDBH9o,8042
216
218
  ai_review/tests/fixtures/clients/ollama.py,sha256=UUHDDPUraQAG8gBC-0UvftaK0BDYir5cJDlRKJymSQg,2109
217
219
  ai_review/tests/fixtures/clients/openai.py,sha256=UgfuRZWzl3X7ZVHMLKP4mZxNXVpcccitkc9tuUyffXE,2267
218
220
  ai_review/tests/fixtures/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -230,8 +232,9 @@ ai_review/tests/fixtures/services/vcs.py,sha256=RXfbuW0Ffx7km7q2Zck__1qTZdZ-_vNr
230
232
  ai_review/tests/fixtures/services/review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
231
233
  ai_review/tests/fixtures/services/review/base.py,sha256=10LaT6xi5IP0CrR6LfatirVKnZ7D0n8Q1dV99nOBEuE,1501
232
234
  ai_review/tests/fixtures/services/review/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
233
- ai_review/tests/fixtures/services/review/gateway/comment.py,sha256=nm_dA3teFK5cZJltKS9UcDlUvxmbClii56CCv4vkRF8,4063
234
- ai_review/tests/fixtures/services/review/gateway/llm.py,sha256=OXYZtJbQ8VqvNlPxG6CLPT0mz-HrhfZA8hoEAgf8loM,515
235
+ ai_review/tests/fixtures/services/review/gateway/review_comment_gateway.py,sha256=aclYEHKS7Kd0TK6_AGvkYsdMFhESHAGhk4Xpblxy1bQ,4078
236
+ ai_review/tests/fixtures/services/review/gateway/review_dry_run_comment_gateway.py,sha256=3E-fDONV4j8GJlSaumqN90Rayoe8yKjnn6APwOrpDDs,4125
237
+ ai_review/tests/fixtures/services/review/gateway/review_llm_gateway.py,sha256=tm3ktE5wFVIFFvHPSUO1wQG7LC3Ekta2PwWky9ugL98,1146
235
238
  ai_review/tests/fixtures/services/review/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
236
239
  ai_review/tests/fixtures/services/review/internal/inline.py,sha256=D-npuBMSsP_ng5RgplaUZnANl3q5f7GD4OkE8IvQv2M,1038
237
240
  ai_review/tests/fixtures/services/review/internal/inline_reply.py,sha256=NRfcVBHavyHvoxOiqMhYhcf6YWDLfT367ZoXeUZIieU,1047
@@ -311,10 +314,11 @@ ai_review/tests/suites/services/prompt/test_schema.py,sha256=rm2__LA2_4qQwSmNAZ_
311
314
  ai_review/tests/suites/services/prompt/test_service.py,sha256=jfpmM--eZH43NylwltX-ijmF58ZJ4WA83kmGshUbJfs,8312
312
315
  ai_review/tests/suites/services/prompt/test_tools.py,sha256=SmweFvrpxd-3RO5v18vCl7zonEqFE1n4eqHH7-6auYM,4511
313
316
  ai_review/tests/suites/services/review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
314
- ai_review/tests/suites/services/review/test_service.py,sha256=1KSgYQ1SfO4dserZTMrkLfAzv0466k7LQG8m4TA_rGk,3324
317
+ ai_review/tests/suites/services/review/test_service.py,sha256=9XBywDoVTzLXlfqMmrxwJbPoj8QoGFW5wDwirqu2UHI,4206
315
318
  ai_review/tests/suites/services/review/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
316
- ai_review/tests/suites/services/review/gateway/test_comment.py,sha256=mFraqWLAF2yTcy8qAwAE5YjuWsab7GixFT5II3Ca7zg,8816
317
- ai_review/tests/suites/services/review/gateway/test_llm.py,sha256=WO326PiZkF4fHPYl1CRDfpUkLsC2BrLPmJpu0rnWrwY,2903
319
+ ai_review/tests/suites/services/review/gateway/test_review_comment_gateway.py,sha256=eueGnKY3YcLvLQ8hSdhKQuVfOlFwxP82cwgEHKyRPiQ,8831
320
+ ai_review/tests/suites/services/review/gateway/test_review_dry_run_comment_gateway.py,sha256=YGSCZRyyV9BJaKpI66viHYZdM-GtuODJt4ZzEW1-29g,3862
321
+ ai_review/tests/suites/services/review/gateway/test_review_llm_gateway.py,sha256=m4HKvqDQYNesY5F2wuOMeHqXwQe99K4K_depdyTs37Q,2508
318
322
  ai_review/tests/suites/services/review/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
319
323
  ai_review/tests/suites/services/review/internal/inline/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
320
324
  ai_review/tests/suites/services/review/internal/inline/test_schema.py,sha256=2SGF8yNHsNv8lwG-MUlgO3RT4juTt0PQSl10qNhSt7o,2377
@@ -331,11 +335,11 @@ ai_review/tests/suites/services/review/internal/summary_reply/__init__.py,sha256
331
335
  ai_review/tests/suites/services/review/internal/summary_reply/test_schema.py,sha256=_2kfcHjB-thIFTIhdDBR7PsEl8beIAu4FtEkQjb0Ljk,859
332
336
  ai_review/tests/suites/services/review/internal/summary_reply/test_service.py,sha256=VlVr9wXXwmpp9qxy_4dMZnFHSZ12j2QSP_m58Vs_wXE,709
333
337
  ai_review/tests/suites/services/review/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
334
- ai_review/tests/suites/services/review/runner/test_context.py,sha256=57RO3lBR35RfLizJ71wdvJ6MAVz9h8M8i7i6ZKF8MNY,3948
335
- ai_review/tests/suites/services/review/runner/test_inline.py,sha256=ZehmjvoNe-0Zff55xBceBBg7tNn5AWRW_MdPNt-Uvrg,4651
336
- ai_review/tests/suites/services/review/runner/test_inline_reply.py,sha256=X_vu1lB83z5k8VG7mfBZaCtfFkq2D4VnGpQEsNJOrmo,4888
337
- ai_review/tests/suites/services/review/runner/test_summary.py,sha256=ePiORxLDtmsIqRA-eAZ8eDjVhVBjALrJ8jjIb6DlshE,3944
338
- ai_review/tests/suites/services/review/runner/test_summary_reply.py,sha256=L-G_Jpf3etnNqgPp9fhCy3JH_gmniyl9IUPGyEvkRLM,4546
338
+ ai_review/tests/suites/services/review/runner/test_context.py,sha256=tuOntcwvgnH89mlFXAnwmERPbw3AMk4NgZ6rnRmWA98,3978
339
+ ai_review/tests/suites/services/review/runner/test_inline.py,sha256=rnnfXqxeEYuzi7n3G9iscAfYst607LzovbtEwV6z0cQ,4681
340
+ ai_review/tests/suites/services/review/runner/test_inline_reply.py,sha256=Q3gsOdWvA1fYKRn-8BII19XAylAKK3B1i20xeP_Z1u4,4918
341
+ ai_review/tests/suites/services/review/runner/test_summary.py,sha256=VLIcKffScWSaxUztYHNLAsNUMGiJQWn7j_Le8Zcrizo,3974
342
+ ai_review/tests/suites/services/review/runner/test_summary_reply.py,sha256=UExBEkWh-EG0akVchgLdnnpcd7HFqEnDyMAVbFY_rtU,4576
339
343
  ai_review/tests/suites/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
340
344
  ai_review/tests/suites/services/vcs/test_factory.py,sha256=EergKSHW4b7RZg9vJJ5Cj0XfPsDTLEclV1kq2_9greA,1138
341
345
  ai_review/tests/suites/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -345,11 +349,11 @@ ai_review/tests/suites/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW
345
349
  ai_review/tests/suites/services/vcs/github/test_adapter.py,sha256=bK4k532v_8kDiud5RI9OlGetWMiLP8NaW1vEfHvcHfQ,4893
346
350
  ai_review/tests/suites/services/vcs/github/test_client.py,sha256=mNt1bA6aVU3REsJiU_tK1PokQxQTaCKun0tNBHuvIp8,8039
347
351
  ai_review/tests/suites/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
348
- ai_review/tests/suites/services/vcs/gitlab/test_adapter.py,sha256=VeORrVExX1rnrQluaBB7LUgmzl_FCwHgW-TAqG4qF1w,3368
349
- ai_review/tests/suites/services/vcs/gitlab/test_client.py,sha256=8qvf0qGZSEa3fTpMMz6fL-JSFw1rQlPda9bjfKXDC7Y,8028
350
- xai_review-0.28.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
351
- xai_review-0.28.0.dist-info/METADATA,sha256=3iCvsSWPGbG6VXl9xM8z7DAbDzACRS0uAf7C7PI5p6A,11358
352
- xai_review-0.28.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
353
- xai_review-0.28.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
354
- xai_review-0.28.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
355
- xai_review-0.28.0.dist-info/RECORD,,
352
+ ai_review/tests/suites/services/vcs/gitlab/test_adapter.py,sha256=BYBP2g1AKF_jCSJYJj16pW7M_6PprwD9reYEpdw3StU,4340
353
+ ai_review/tests/suites/services/vcs/gitlab/test_client.py,sha256=dnI-YxYADmVF2GS9rp6-JPkcqsn4sN8Fjbe4MkeYMaE,8476
354
+ xai_review-0.30.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
355
+ xai_review-0.30.0.dist-info/METADATA,sha256=-Tmw1DyUEv-KEMfBUJCj59yzMM8STdCO-A-ZrCRet6Y,11358
356
+ xai_review-0.30.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
357
+ xai_review-0.30.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
358
+ xai_review-0.30.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
359
+ xai_review-0.30.0.dist-info/RECORD,,
@@ -1,17 +0,0 @@
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()