xai-review 0.29.0__py3-none-any.whl → 0.31.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 (50) hide show
  1. ai_review/clients/gitea/__init__.py +0 -0
  2. ai_review/clients/gitea/client.py +31 -0
  3. ai_review/clients/gitea/pr/__init__.py +0 -0
  4. ai_review/clients/gitea/pr/client.py +118 -0
  5. ai_review/clients/gitea/pr/schema/__init__.py +0 -0
  6. ai_review/clients/gitea/pr/schema/comments.py +37 -0
  7. ai_review/clients/gitea/pr/schema/files.py +16 -0
  8. ai_review/clients/gitea/pr/schema/pull_request.py +18 -0
  9. ai_review/clients/gitea/pr/schema/user.py +6 -0
  10. ai_review/clients/gitea/pr/types.py +25 -0
  11. ai_review/clients/gitea/tools.py +6 -0
  12. ai_review/libs/config/review.py +1 -0
  13. ai_review/libs/config/vcs/base.py +8 -1
  14. ai_review/libs/config/vcs/gitea.py +13 -0
  15. ai_review/libs/constants/vcs_provider.py +1 -0
  16. ai_review/services/review/gateway/review_dry_run_comment_gateway.py +42 -0
  17. ai_review/services/review/service.py +9 -3
  18. ai_review/services/vcs/factory.py +3 -0
  19. ai_review/services/vcs/gitea/__init__.py +0 -0
  20. ai_review/services/vcs/gitea/adapter.py +22 -0
  21. ai_review/services/vcs/gitea/client.py +151 -0
  22. ai_review/tests/fixtures/clients/gitea.py +141 -0
  23. ai_review/tests/fixtures/services/review/gateway/{comment.py → review_comment_gateway.py} +1 -1
  24. ai_review/tests/fixtures/services/review/gateway/review_dry_run_comment_gateway.py +103 -0
  25. ai_review/tests/fixtures/services/review/gateway/review_llm_gateway.py +34 -0
  26. ai_review/tests/suites/clients/gitea/__init__.py +0 -0
  27. ai_review/tests/suites/clients/gitea/test_client.py +14 -0
  28. ai_review/tests/suites/clients/gitea/test_tools.py +26 -0
  29. ai_review/tests/suites/services/review/gateway/{test_comment.py → test_review_comment_gateway.py} +1 -1
  30. ai_review/tests/suites/services/review/gateway/test_review_dry_run_comment_gateway.py +93 -0
  31. ai_review/tests/suites/services/review/gateway/{test_llm.py → test_review_llm_gateway.py} +1 -15
  32. ai_review/tests/suites/services/review/runner/test_context.py +2 -2
  33. ai_review/tests/suites/services/review/runner/test_inline.py +2 -2
  34. ai_review/tests/suites/services/review/runner/test_inline_reply.py +2 -2
  35. ai_review/tests/suites/services/review/runner/test_summary.py +2 -2
  36. ai_review/tests/suites/services/review/runner/test_summary_reply.py +2 -2
  37. ai_review/tests/suites/services/review/test_service.py +18 -0
  38. ai_review/tests/suites/services/vcs/gitea/__init__.py +0 -0
  39. ai_review/tests/suites/services/vcs/gitea/test_adapter.py +52 -0
  40. ai_review/tests/suites/services/vcs/gitea/test_client.py +86 -0
  41. ai_review/tests/suites/services/vcs/test_factory.py +7 -0
  42. {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/METADATA +21 -12
  43. {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/RECORD +49 -24
  44. ai_review/tests/fixtures/services/review/gateway/llm.py +0 -17
  45. /ai_review/services/review/gateway/{comment.py → review_comment_gateway.py} +0 -0
  46. /ai_review/services/review/gateway/{llm.py → review_llm_gateway.py} +0 -0
  47. {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/WHEEL +0 -0
  48. {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/entry_points.txt +0 -0
  49. {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/licenses/LICENSE +0 -0
  50. {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,141 @@
1
+ import pytest
2
+ from pydantic import HttpUrl, SecretStr
3
+
4
+ from ai_review.clients.gitea.pr.schema.comments import (
5
+ GiteaPRCommentSchema,
6
+ GiteaGetPRCommentsResponseSchema,
7
+ GiteaCreateCommentRequestSchema,
8
+ GiteaCreateCommentResponseSchema,
9
+ )
10
+ from ai_review.clients.gitea.pr.schema.files import (
11
+ GiteaGetPRFilesResponseSchema,
12
+ GiteaPRFileSchema,
13
+ )
14
+ from ai_review.clients.gitea.pr.schema.pull_request import (
15
+ GiteaGetPRResponseSchema,
16
+ GiteaBranchSchema,
17
+ )
18
+ from ai_review.clients.gitea.pr.schema.user import GiteaUserSchema
19
+ from ai_review.clients.gitea.pr.types import GiteaPullRequestsHTTPClientProtocol
20
+ from ai_review.config import settings
21
+ from ai_review.libs.config.vcs.base import GiteaVCSConfig
22
+ from ai_review.libs.config.vcs.gitea import GiteaPipelineConfig, GiteaHTTPClientConfig
23
+ from ai_review.libs.constants.vcs_provider import VCSProvider
24
+ from ai_review.services.vcs.gitea.client import GiteaVCSClient
25
+
26
+
27
+ class FakeGiteaPullRequestsHTTPClient(GiteaPullRequestsHTTPClientProtocol):
28
+ def __init__(self):
29
+ self.calls: list[tuple[str, dict]] = []
30
+
31
+ async def get_pull_request(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRResponseSchema:
32
+ self.calls.append(("get_pull_request", {"owner": owner, "repo": repo, "pull_number": pull_number}))
33
+ return GiteaGetPRResponseSchema(
34
+ id=1,
35
+ number=1,
36
+ title="Fake Gitea PR",
37
+ body="This is a fake PR for testing",
38
+ user=GiteaUserSchema(id=101, login="tester"),
39
+ base=GiteaBranchSchema(ref="main", sha="abc123"),
40
+ head=GiteaBranchSchema(ref="feature", sha="def456"),
41
+ )
42
+
43
+ async def get_files(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRFilesResponseSchema:
44
+ self.calls.append(("get_files", {"owner": owner, "repo": repo, "pull_number": pull_number}))
45
+ return GiteaGetPRFilesResponseSchema(
46
+ root=[
47
+ GiteaPRFileSchema(
48
+ sha="abc",
49
+ status="modified",
50
+ filename="src/main.py",
51
+ patch="@@ -1,2 +1,2 @@\n- old\n+ new",
52
+ ),
53
+ GiteaPRFileSchema(
54
+ sha="def",
55
+ status="added",
56
+ filename="utils/helper.py",
57
+ patch="+ print('Hello')",
58
+ ),
59
+ ]
60
+ )
61
+
62
+ async def get_comments(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRCommentsResponseSchema:
63
+ self.calls.append(("get_comments", {"owner": owner, "repo": repo, "pull_number": pull_number}))
64
+ return GiteaGetPRCommentsResponseSchema(
65
+ root=[
66
+ GiteaPRCommentSchema(
67
+ id=1,
68
+ body="General comment",
69
+ user=GiteaUserSchema(id=201, login="alice"),
70
+ ),
71
+ GiteaPRCommentSchema(
72
+ id=2,
73
+ body="Inline comment",
74
+ path="file.py",
75
+ line=5,
76
+ user=GiteaUserSchema(id=202, login="bob"),
77
+ ),
78
+ ]
79
+ )
80
+
81
+ async def create_comment(
82
+ self,
83
+ owner: str,
84
+ repo: str,
85
+ pull_number: str,
86
+ request: GiteaCreateCommentRequestSchema
87
+ ) -> GiteaCreateCommentResponseSchema:
88
+ self.calls.append(
89
+ (
90
+ "create_comment",
91
+ {"owner": owner, "repo": repo, "pull_number": pull_number, **request.model_dump()},
92
+ )
93
+ )
94
+ return GiteaCreateCommentResponseSchema(id=10, body=request.body)
95
+
96
+
97
+ class FakeGiteaHTTPClient:
98
+ def __init__(self, pull_requests_client: FakeGiteaPullRequestsHTTPClient):
99
+ self.pr = pull_requests_client
100
+
101
+
102
+ @pytest.fixture
103
+ def fake_gitea_pull_requests_http_client() -> FakeGiteaPullRequestsHTTPClient:
104
+ return FakeGiteaPullRequestsHTTPClient()
105
+
106
+
107
+ @pytest.fixture
108
+ def fake_gitea_http_client(
109
+ fake_gitea_pull_requests_http_client: FakeGiteaPullRequestsHTTPClient
110
+ ) -> FakeGiteaHTTPClient:
111
+ return FakeGiteaHTTPClient(pull_requests_client=fake_gitea_pull_requests_http_client)
112
+
113
+
114
+ @pytest.fixture
115
+ def gitea_vcs_client(
116
+ monkeypatch: pytest.MonkeyPatch,
117
+ fake_gitea_http_client: FakeGiteaHTTPClient
118
+ ) -> GiteaVCSClient:
119
+ monkeypatch.setattr(
120
+ "ai_review.services.vcs.gitea.client.get_gitea_http_client",
121
+ lambda: fake_gitea_http_client,
122
+ )
123
+ return GiteaVCSClient()
124
+
125
+
126
+ @pytest.fixture
127
+ def gitea_http_client_config(monkeypatch: pytest.MonkeyPatch):
128
+ fake_config = GiteaVCSConfig(
129
+ provider=VCSProvider.GITEA,
130
+ pipeline=GiteaPipelineConfig(
131
+ repo="repo",
132
+ owner="owner",
133
+ pull_number="1",
134
+ ),
135
+ http_client=GiteaHTTPClientConfig(
136
+ timeout=10,
137
+ api_url=HttpUrl("https://gitea.example.com"),
138
+ api_token=SecretStr("fake-token"),
139
+ ),
140
+ )
141
+ monkeypatch.setattr(settings, "vcs", fake_config)
@@ -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
+ )
File without changes
@@ -0,0 +1,14 @@
1
+ import pytest
2
+ from httpx import AsyncClient
3
+
4
+ from ai_review.clients.gitea.client import get_gitea_http_client, GiteaHTTPClient
5
+ from ai_review.clients.gitea.pr.client import GiteaPullRequestsHTTPClient
6
+
7
+
8
+ @pytest.mark.usefixtures("gitea_http_client_config")
9
+ def test_get_gitea_http_client_builds_ok():
10
+ gitea_http_client = get_gitea_http_client()
11
+
12
+ assert isinstance(gitea_http_client, GiteaHTTPClient)
13
+ assert isinstance(gitea_http_client.pr, GiteaPullRequestsHTTPClient)
14
+ assert isinstance(gitea_http_client.pr.client, AsyncClient)
@@ -0,0 +1,26 @@
1
+ from httpx import Response, Request
2
+
3
+ from ai_review.clients.gitea.tools import gitea_has_next_page
4
+
5
+
6
+ def make_response(headers: dict) -> Response:
7
+ return Response(
8
+ request=Request("GET", "http://gitea.test"),
9
+ headers=headers,
10
+ status_code=200,
11
+ )
12
+
13
+
14
+ def test_gitea_has_next_page_true():
15
+ resp = make_response({"Link": '<https://gitea.test?page=2>; rel="next"'})
16
+ assert gitea_has_next_page(resp) is True
17
+
18
+
19
+ def test_gitea_has_next_page_false_empty():
20
+ resp = make_response({"Link": ""})
21
+ assert gitea_has_next_page(resp) is False
22
+
23
+
24
+ def test_gitea_has_next_page_false_missing():
25
+ resp = make_response({})
26
+ assert gitea_has_next_page(resp) is False
@@ -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
File without changes
@@ -0,0 +1,52 @@
1
+ from ai_review.clients.gitea.pr.schema.comments import GiteaPRCommentSchema
2
+ from ai_review.clients.gitea.pr.schema.user import GiteaUserSchema
3
+ from ai_review.services.vcs.gitea.adapter import get_review_comment_from_gitea_comment, get_user_from_gitea_user
4
+ from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
5
+
6
+
7
+ def test_get_user_from_gitea_user_maps_fields_correctly():
8
+ user = GiteaUserSchema(id=42, login="tester")
9
+ result = get_user_from_gitea_user(user)
10
+
11
+ assert isinstance(result, UserSchema)
12
+ assert result.id == 42
13
+ assert result.username == "tester"
14
+ assert result.name == "tester"
15
+
16
+
17
+ def test_get_user_from_gitea_user_handles_none():
18
+ result = get_user_from_gitea_user(None)
19
+ assert result.id is None
20
+ assert result.username == ""
21
+ assert result.name == ""
22
+
23
+
24
+ def test_get_review_comment_from_gitea_comment_maps_all_fields():
25
+ comment = GiteaPRCommentSchema(
26
+ id=10,
27
+ body="Inline comment",
28
+ path="src/main.py",
29
+ line=15,
30
+ user=GiteaUserSchema(id=1, login="dev"),
31
+ )
32
+
33
+ result = get_review_comment_from_gitea_comment(comment)
34
+
35
+ assert isinstance(result, ReviewCommentSchema)
36
+ assert result.id == 10
37
+ assert result.body == "Inline comment"
38
+ assert result.file == "src/main.py"
39
+ assert result.line == 15
40
+ assert result.thread_id == 10
41
+ assert isinstance(result.author, UserSchema)
42
+ assert result.author.username == "dev"
43
+
44
+
45
+ def test_get_review_comment_handles_missing_user_and_body():
46
+ comment = GiteaPRCommentSchema(id=11, body="", path=None, line=None, user=None)
47
+
48
+ result = get_review_comment_from_gitea_comment(comment)
49
+ assert result.body == ""
50
+ assert result.author.username == ""
51
+ assert result.file is None
52
+ assert result.line is None