xai-review 0.37.0__py3-none-any.whl → 0.39.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.
- ai_review/clients/{bitbucket → bitbucket_cloud}/client.py +6 -6
- ai_review/clients/{bitbucket → bitbucket_cloud}/pr/client.py +52 -40
- ai_review/clients/bitbucket_cloud/pr/schema/comments.py +59 -0
- ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/files.py +7 -7
- ai_review/clients/bitbucket_cloud/pr/schema/pull_request.py +34 -0
- ai_review/clients/bitbucket_cloud/pr/schema/user.py +7 -0
- ai_review/clients/bitbucket_cloud/pr/types.py +44 -0
- ai_review/clients/{bitbucket → bitbucket_cloud}/tools.py +1 -1
- ai_review/clients/bitbucket_server/client.py +32 -0
- ai_review/clients/bitbucket_server/pr/client.py +163 -0
- ai_review/clients/bitbucket_server/pr/schema/changes.py +36 -0
- ai_review/clients/bitbucket_server/pr/schema/comments.py +55 -0
- ai_review/clients/bitbucket_server/pr/schema/pull_request.py +48 -0
- ai_review/clients/bitbucket_server/pr/schema/user.py +13 -0
- ai_review/clients/bitbucket_server/pr/types.py +44 -0
- ai_review/clients/bitbucket_server/tools.py +6 -0
- ai_review/libs/config/vcs/base.py +23 -6
- ai_review/libs/config/vcs/{bitbucket.py → bitbucket_cloud.py} +2 -2
- ai_review/libs/config/vcs/bitbucket_server.py +13 -0
- ai_review/libs/constants/vcs_provider.py +2 -1
- ai_review/libs/http/client.py +1 -1
- ai_review/services/vcs/bitbucket_cloud/__init__.py +0 -0
- ai_review/services/vcs/{bitbucket → bitbucket_cloud}/adapter.py +4 -4
- ai_review/services/vcs/{bitbucket → bitbucket_cloud}/client.py +26 -23
- ai_review/services/vcs/bitbucket_server/__init__.py +0 -0
- ai_review/services/vcs/bitbucket_server/adapter.py +27 -0
- ai_review/services/vcs/bitbucket_server/client.py +263 -0
- ai_review/services/vcs/factory.py +6 -3
- ai_review/tests/fixtures/clients/bitbucket_cloud.py +207 -0
- ai_review/tests/fixtures/clients/bitbucket_server.py +265 -0
- ai_review/tests/suites/clients/bitbucket_cloud/__init__.py +0 -0
- ai_review/tests/suites/clients/bitbucket_cloud/test_client.py +14 -0
- ai_review/tests/suites/clients/bitbucket_cloud/test_tools.py +31 -0
- ai_review/tests/suites/clients/bitbucket_server/__init__.py +0 -0
- ai_review/tests/suites/clients/bitbucket_server/test_client.py +14 -0
- ai_review/tests/suites/clients/bitbucket_server/test_tools.py +38 -0
- ai_review/tests/suites/services/vcs/bitbucket_cloud/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/{bitbucket → bitbucket_cloud}/test_adapter.py +24 -24
- ai_review/tests/suites/services/vcs/{bitbucket → bitbucket_cloud}/test_client.py +51 -51
- ai_review/tests/suites/services/vcs/bitbucket_server/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/bitbucket_server/test_adapter.py +115 -0
- ai_review/tests/suites/services/vcs/bitbucket_server/test_client.py +201 -0
- ai_review/tests/suites/services/vcs/test_factory.py +11 -4
- {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/METADATA +9 -7
- {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/RECORD +55 -33
- ai_review/clients/bitbucket/pr/schema/comments.py +0 -63
- ai_review/clients/bitbucket/pr/schema/pull_request.py +0 -34
- ai_review/clients/bitbucket/pr/schema/user.py +0 -7
- ai_review/clients/bitbucket/pr/types.py +0 -44
- ai_review/tests/fixtures/clients/bitbucket.py +0 -204
- ai_review/tests/suites/clients/bitbucket/test_client.py +0 -14
- ai_review/tests/suites/clients/bitbucket/test_tools.py +0 -31
- /ai_review/clients/{bitbucket → bitbucket_cloud}/__init__.py +0 -0
- /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/__init__.py +0 -0
- /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/__init__.py +0 -0
- /ai_review/{services/vcs/bitbucket → clients/bitbucket_server}/__init__.py +0 -0
- /ai_review/{tests/suites/clients/bitbucket → clients/bitbucket_server/pr}/__init__.py +0 -0
- /ai_review/{tests/suites/services/vcs/bitbucket → clients/bitbucket_server/pr/schema}/__init__.py +0 -0
- {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/WHEEL +0 -0
- {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
-
from ai_review.services.vcs.
|
|
3
|
+
from ai_review.services.vcs.bitbucket_cloud.client import BitbucketCloudVCSClient
|
|
4
4
|
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema, ReviewThreadSchema, ThreadKind
|
|
5
|
-
from ai_review.tests.fixtures.clients.
|
|
5
|
+
from ai_review.tests.fixtures.clients.bitbucket_cloud import FakeBitbucketCloudPullRequestsHTTPClient
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@pytest.mark.asyncio
|
|
9
|
-
@pytest.mark.usefixtures("
|
|
9
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
10
10
|
async def test_get_review_info_returns_valid_schema(
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
12
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
13
13
|
):
|
|
14
14
|
"""Should return detailed PR info with branches, author, reviewers, and files."""
|
|
15
|
-
info = await
|
|
15
|
+
info = await bitbucket_cloud_vcs_client.get_review_info()
|
|
16
16
|
|
|
17
17
|
assert isinstance(info, ReviewInfoSchema)
|
|
18
18
|
assert info.id == 1
|
|
@@ -20,7 +20,7 @@ async def test_get_review_info_returns_valid_schema(
|
|
|
20
20
|
assert info.description == "This is a fake PR for testing"
|
|
21
21
|
|
|
22
22
|
assert info.author.username == "tester"
|
|
23
|
-
assert {
|
|
23
|
+
assert {reviewer.username for reviewer in info.reviewers} == {"reviewer"}
|
|
24
24
|
|
|
25
25
|
assert info.source_branch.ref == "feature/test"
|
|
26
26
|
assert info.target_branch.ref == "main"
|
|
@@ -30,20 +30,20 @@ async def test_get_review_info_returns_valid_schema(
|
|
|
30
30
|
assert "app/main.py" in info.changed_files
|
|
31
31
|
assert len(info.changed_files) == 2
|
|
32
32
|
|
|
33
|
-
called_methods = [name for name, _ in
|
|
33
|
+
called_methods = [name for name, _ in fake_bitbucket_cloud_pull_requests_http_client.calls]
|
|
34
34
|
assert called_methods == ["get_pull_request", "get_files"]
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@pytest.mark.asyncio
|
|
38
|
-
@pytest.mark.usefixtures("
|
|
38
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
39
39
|
async def test_get_general_comments_filters_inline(
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
41
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
42
42
|
):
|
|
43
43
|
"""Should return only general comments (without inline info)."""
|
|
44
|
-
comments = await
|
|
44
|
+
comments = await bitbucket_cloud_vcs_client.get_general_comments()
|
|
45
45
|
|
|
46
|
-
assert all(isinstance(
|
|
46
|
+
assert all(isinstance(comment, ReviewCommentSchema) for comment in comments)
|
|
47
47
|
assert len(comments) == 1
|
|
48
48
|
|
|
49
49
|
first = comments[0]
|
|
@@ -51,20 +51,20 @@ async def test_get_general_comments_filters_inline(
|
|
|
51
51
|
assert first.file is None
|
|
52
52
|
assert first.line is None
|
|
53
53
|
|
|
54
|
-
called_methods = [name for name, _ in
|
|
54
|
+
called_methods = [name for name, _ in fake_bitbucket_cloud_pull_requests_http_client.calls]
|
|
55
55
|
assert called_methods == ["get_comments"]
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
@pytest.mark.asyncio
|
|
59
|
-
@pytest.mark.usefixtures("
|
|
59
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
60
60
|
async def test_get_inline_comments_filters_general(
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
62
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
63
63
|
):
|
|
64
64
|
"""Should return only inline comments with file and line references."""
|
|
65
|
-
comments = await
|
|
65
|
+
comments = await bitbucket_cloud_vcs_client.get_inline_comments()
|
|
66
66
|
|
|
67
|
-
assert all(isinstance(
|
|
67
|
+
assert all(isinstance(comment, ReviewCommentSchema) for comment in comments)
|
|
68
68
|
assert len(comments) == 1
|
|
69
69
|
|
|
70
70
|
first = comments[0]
|
|
@@ -72,22 +72,22 @@ async def test_get_inline_comments_filters_general(
|
|
|
72
72
|
assert first.file == "file.py"
|
|
73
73
|
assert first.line == 5
|
|
74
74
|
|
|
75
|
-
called_methods = [name for name, _ in
|
|
75
|
+
called_methods = [name for name, _ in fake_bitbucket_cloud_pull_requests_http_client.calls]
|
|
76
76
|
assert called_methods == ["get_comments"]
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
@pytest.mark.asyncio
|
|
80
|
-
@pytest.mark.usefixtures("
|
|
80
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
81
81
|
async def test_create_general_comment_posts_comment(
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
83
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
84
84
|
):
|
|
85
85
|
"""Should post a general (non-inline) comment."""
|
|
86
86
|
message = "Hello from Bitbucket test!"
|
|
87
87
|
|
|
88
|
-
await
|
|
88
|
+
await bitbucket_cloud_vcs_client.create_general_comment(message)
|
|
89
89
|
|
|
90
|
-
calls = [args for name, args in
|
|
90
|
+
calls = [args for name, args in fake_bitbucket_cloud_pull_requests_http_client.calls if name == "create_comment"]
|
|
91
91
|
assert len(calls) == 1
|
|
92
92
|
call_args = calls[0]
|
|
93
93
|
assert call_args["content"]["raw"] == message
|
|
@@ -96,19 +96,19 @@ async def test_create_general_comment_posts_comment(
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
@pytest.mark.asyncio
|
|
99
|
-
@pytest.mark.usefixtures("
|
|
99
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
100
100
|
async def test_create_inline_comment_posts_comment(
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
102
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
103
103
|
):
|
|
104
104
|
"""Should post an inline comment with correct file and line."""
|
|
105
105
|
file = "file.py"
|
|
106
106
|
line = 10
|
|
107
107
|
message = "Looks good"
|
|
108
108
|
|
|
109
|
-
await
|
|
109
|
+
await bitbucket_cloud_vcs_client.create_inline_comment(file, line, message)
|
|
110
110
|
|
|
111
|
-
calls = [args for name, args in
|
|
111
|
+
calls = [args for name, args in fake_bitbucket_cloud_pull_requests_http_client.calls if name == "create_comment"]
|
|
112
112
|
assert len(calls) == 1
|
|
113
113
|
|
|
114
114
|
call_args = calls[0]
|
|
@@ -118,18 +118,18 @@ async def test_create_inline_comment_posts_comment(
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
@pytest.mark.asyncio
|
|
121
|
-
@pytest.mark.usefixtures("
|
|
121
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
122
122
|
async def test_create_inline_reply_posts_comment(
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
124
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
125
125
|
):
|
|
126
126
|
"""Should post a reply to an existing inline thread."""
|
|
127
127
|
thread_id = 42
|
|
128
128
|
message = "I agree with this inline comment."
|
|
129
129
|
|
|
130
|
-
await
|
|
130
|
+
await bitbucket_cloud_vcs_client.create_inline_reply(thread_id, message)
|
|
131
131
|
|
|
132
|
-
calls = [args for name, args in
|
|
132
|
+
calls = [args for name, args in fake_bitbucket_cloud_pull_requests_http_client.calls if name == "create_comment"]
|
|
133
133
|
assert len(calls) == 1
|
|
134
134
|
|
|
135
135
|
call_args = calls[0]
|
|
@@ -140,18 +140,18 @@ async def test_create_inline_reply_posts_comment(
|
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
@pytest.mark.asyncio
|
|
143
|
-
@pytest.mark.usefixtures("
|
|
143
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
144
144
|
async def test_create_summary_reply_posts_comment_with_parent(
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
146
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
147
147
|
):
|
|
148
148
|
"""Should post a reply to a general thread (same API with parent id)."""
|
|
149
149
|
thread_id = 7
|
|
150
150
|
message = "Thanks for the clarification."
|
|
151
151
|
|
|
152
|
-
await
|
|
152
|
+
await bitbucket_cloud_vcs_client.create_summary_reply(thread_id, message)
|
|
153
153
|
|
|
154
|
-
calls = [args for name, args in
|
|
154
|
+
calls = [args for name, args in fake_bitbucket_cloud_pull_requests_http_client.calls if name == "create_comment"]
|
|
155
155
|
assert len(calls) == 1
|
|
156
156
|
|
|
157
157
|
call_args = calls[0]
|
|
@@ -162,13 +162,13 @@ async def test_create_summary_reply_posts_comment_with_parent(
|
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
@pytest.mark.asyncio
|
|
165
|
-
@pytest.mark.usefixtures("
|
|
165
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
166
166
|
async def test_get_inline_threads_groups_by_thread_id(
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
168
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
169
169
|
):
|
|
170
170
|
"""Should group inline comments into threads."""
|
|
171
|
-
threads = await
|
|
171
|
+
threads = await bitbucket_cloud_vcs_client.get_inline_threads()
|
|
172
172
|
|
|
173
173
|
assert all(isinstance(thread, ReviewThreadSchema) for thread in threads)
|
|
174
174
|
assert len(threads) == 1
|
|
@@ -180,25 +180,25 @@ async def test_get_inline_threads_groups_by_thread_id(
|
|
|
180
180
|
assert len(thread.comments) == 1
|
|
181
181
|
assert isinstance(thread.comments[0], ReviewCommentSchema)
|
|
182
182
|
|
|
183
|
-
called_methods = [name for name, _ in
|
|
183
|
+
called_methods = [name for name, _ in fake_bitbucket_cloud_pull_requests_http_client.calls]
|
|
184
184
|
assert "get_comments" in called_methods
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
@pytest.mark.asyncio
|
|
188
|
-
@pytest.mark.usefixtures("
|
|
188
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
189
189
|
async def test_get_general_threads_groups_by_thread_id(
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
bitbucket_cloud_vcs_client: BitbucketCloudVCSClient,
|
|
191
|
+
fake_bitbucket_cloud_pull_requests_http_client: FakeBitbucketCloudPullRequestsHTTPClient,
|
|
192
192
|
):
|
|
193
193
|
"""Should group general (non-inline) comments into SUMMARY threads."""
|
|
194
|
-
threads = await
|
|
194
|
+
threads = await bitbucket_cloud_vcs_client.get_general_threads()
|
|
195
195
|
|
|
196
|
-
assert all(isinstance(
|
|
196
|
+
assert all(isinstance(thread, ReviewThreadSchema) for thread in threads)
|
|
197
197
|
assert len(threads) == 1
|
|
198
198
|
thread = threads[0]
|
|
199
199
|
assert thread.kind == ThreadKind.SUMMARY
|
|
200
200
|
assert len(thread.comments) == 1
|
|
201
201
|
assert isinstance(thread.comments[0], ReviewCommentSchema)
|
|
202
202
|
|
|
203
|
-
called_methods = [name for name, _ in
|
|
203
|
+
called_methods = [name for name, _ in fake_bitbucket_cloud_pull_requests_http_client.calls]
|
|
204
204
|
assert "get_comments" in called_methods
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from ai_review.clients.bitbucket_server.pr.schema.comments import (
|
|
2
|
+
BitbucketServerCommentSchema,
|
|
3
|
+
BitbucketServerCommentAnchorSchema,
|
|
4
|
+
)
|
|
5
|
+
from ai_review.clients.bitbucket_server.pr.schema.user import BitbucketServerUserSchema
|
|
6
|
+
from ai_review.services.vcs.bitbucket_server.adapter import get_review_comment_from_bitbucket_server_comment
|
|
7
|
+
from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_maps_all_fields_correctly():
|
|
11
|
+
"""Should map Bitbucket Server comment with all fields correctly."""
|
|
12
|
+
comment = BitbucketServerCommentSchema(
|
|
13
|
+
id=101,
|
|
14
|
+
text="Looks good",
|
|
15
|
+
author=BitbucketServerUserSchema(
|
|
16
|
+
id=1,
|
|
17
|
+
name="alice",
|
|
18
|
+
slug="alice",
|
|
19
|
+
display_name="Alice",
|
|
20
|
+
),
|
|
21
|
+
anchor=BitbucketServerCommentAnchorSchema(path="src/utils.py", line=10, line_type="ADDED"),
|
|
22
|
+
comments=[],
|
|
23
|
+
created_date=1690000000,
|
|
24
|
+
updated_date=1690000001,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
result = get_review_comment_from_bitbucket_server_comment(comment)
|
|
28
|
+
|
|
29
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
30
|
+
assert result.id == 101
|
|
31
|
+
assert result.body == "Looks good"
|
|
32
|
+
assert result.file == "src/utils.py"
|
|
33
|
+
assert result.line == 10
|
|
34
|
+
assert result.parent_id is None
|
|
35
|
+
assert result.thread_id == 101
|
|
36
|
+
|
|
37
|
+
assert isinstance(result.author, UserSchema)
|
|
38
|
+
assert result.author.id == 1
|
|
39
|
+
assert result.author.name == "Alice"
|
|
40
|
+
assert result.author.username == "alice"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_maps_author_with_missing_fields():
|
|
44
|
+
"""Should handle partially filled author fields gracefully."""
|
|
45
|
+
comment = BitbucketServerCommentSchema(
|
|
46
|
+
id=202,
|
|
47
|
+
text="Anonymous-like comment",
|
|
48
|
+
author=BitbucketServerUserSchema(
|
|
49
|
+
id=None,
|
|
50
|
+
name="",
|
|
51
|
+
slug=None,
|
|
52
|
+
display_name="",
|
|
53
|
+
),
|
|
54
|
+
anchor=BitbucketServerCommentAnchorSchema(path="src/app.py", line=15, line_type="ADDED"),
|
|
55
|
+
comments=[],
|
|
56
|
+
created_date=1690000004,
|
|
57
|
+
updated_date=1690000005,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
result = get_review_comment_from_bitbucket_server_comment(comment)
|
|
61
|
+
|
|
62
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
63
|
+
assert result.author.id is None
|
|
64
|
+
assert result.author.name == ""
|
|
65
|
+
assert result.author.username == ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_maps_without_anchor():
|
|
69
|
+
"""Should handle missing anchor gracefully."""
|
|
70
|
+
comment = BitbucketServerCommentSchema(
|
|
71
|
+
id=303,
|
|
72
|
+
text="General feedback",
|
|
73
|
+
author=BitbucketServerUserSchema(
|
|
74
|
+
id=4,
|
|
75
|
+
name="dave",
|
|
76
|
+
slug="dave",
|
|
77
|
+
display_name="Dave",
|
|
78
|
+
),
|
|
79
|
+
anchor=None,
|
|
80
|
+
comments=[],
|
|
81
|
+
created_date=1690000006,
|
|
82
|
+
updated_date=1690000007,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result = get_review_comment_from_bitbucket_server_comment(comment)
|
|
86
|
+
|
|
87
|
+
assert result.file is None
|
|
88
|
+
assert result.line is None
|
|
89
|
+
assert result.thread_id == 303
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_maps_empty_text_defaults_to_empty_body():
|
|
93
|
+
"""Should default empty text to empty body."""
|
|
94
|
+
comment = BitbucketServerCommentSchema(
|
|
95
|
+
id=404,
|
|
96
|
+
text="",
|
|
97
|
+
author=BitbucketServerUserSchema(
|
|
98
|
+
id=7,
|
|
99
|
+
name="ghost",
|
|
100
|
+
slug="ghost",
|
|
101
|
+
display_name="Ghost",
|
|
102
|
+
),
|
|
103
|
+
anchor=None,
|
|
104
|
+
comments=[],
|
|
105
|
+
created_date=1690000008,
|
|
106
|
+
updated_date=1690000009,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
result = get_review_comment_from_bitbucket_server_comment(comment)
|
|
110
|
+
|
|
111
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
112
|
+
assert result.body == ""
|
|
113
|
+
assert result.file is None
|
|
114
|
+
assert result.line is None
|
|
115
|
+
assert result.thread_id == 404
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.vcs.bitbucket_server.client import BitbucketServerVCSClient
|
|
4
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema, ReviewThreadSchema, ThreadKind
|
|
5
|
+
from ai_review.tests.fixtures.clients.bitbucket_server import FakeBitbucketServerPullRequestsHTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
10
|
+
async def test_get_review_info_returns_valid_schema(
|
|
11
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
12
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
13
|
+
):
|
|
14
|
+
"""Should return detailed PR info with branches, author, reviewers, and files."""
|
|
15
|
+
info = await bitbucket_server_vcs_client.get_review_info()
|
|
16
|
+
|
|
17
|
+
assert isinstance(info, ReviewInfoSchema)
|
|
18
|
+
assert info.id == 1
|
|
19
|
+
assert info.title == "Fake Bitbucket Server PR"
|
|
20
|
+
assert info.description == "PR for testing server client"
|
|
21
|
+
|
|
22
|
+
assert info.author.username == "author"
|
|
23
|
+
assert {reviewer.username for reviewer in info.reviewers} == {"reviewer"}
|
|
24
|
+
|
|
25
|
+
assert info.source_branch.ref == "feature/test"
|
|
26
|
+
assert info.target_branch.ref == "main"
|
|
27
|
+
assert info.base_sha == "abc123"
|
|
28
|
+
assert info.head_sha == "def456"
|
|
29
|
+
|
|
30
|
+
assert "src/main.py" in info.changed_files
|
|
31
|
+
assert len(info.changed_files) == 1
|
|
32
|
+
|
|
33
|
+
called_methods = [name for name, _ in fake_bitbucket_server_pull_requests_http_client.calls]
|
|
34
|
+
assert called_methods == ["get_pull_request", "get_changes"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
39
|
+
async def test_get_general_comments_filters_inline(
|
|
40
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
41
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
42
|
+
):
|
|
43
|
+
"""Should return only general comments (without anchor)."""
|
|
44
|
+
comments = await bitbucket_server_vcs_client.get_general_comments()
|
|
45
|
+
|
|
46
|
+
assert all(isinstance(comment, ReviewCommentSchema) for comment in comments)
|
|
47
|
+
assert len(comments) == 1
|
|
48
|
+
|
|
49
|
+
first = comments[0]
|
|
50
|
+
assert first.body == "General comment"
|
|
51
|
+
assert first.file is None
|
|
52
|
+
assert first.line is None
|
|
53
|
+
|
|
54
|
+
called_methods = [name for name, _ in fake_bitbucket_server_pull_requests_http_client.calls]
|
|
55
|
+
assert called_methods == ["get_comments"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
60
|
+
async def test_get_inline_comments_filters_general(
|
|
61
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
62
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
63
|
+
):
|
|
64
|
+
"""Should return only inline comments with file and line references."""
|
|
65
|
+
comments = await bitbucket_server_vcs_client.get_inline_comments()
|
|
66
|
+
|
|
67
|
+
assert all(isinstance(comment, ReviewCommentSchema) for comment in comments)
|
|
68
|
+
assert len(comments) == 1
|
|
69
|
+
|
|
70
|
+
first = comments[0]
|
|
71
|
+
assert first.body == "Inline comment"
|
|
72
|
+
assert first.file == "src/main.py"
|
|
73
|
+
assert first.line == 5
|
|
74
|
+
|
|
75
|
+
called_methods = [name for name, _ in fake_bitbucket_server_pull_requests_http_client.calls]
|
|
76
|
+
assert called_methods == ["get_comments"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
81
|
+
async def test_create_general_comment_posts_comment(
|
|
82
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
83
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
84
|
+
):
|
|
85
|
+
"""Should post a general (non-inline) comment."""
|
|
86
|
+
message = "Hello from Bitbucket Server test!"
|
|
87
|
+
|
|
88
|
+
await bitbucket_server_vcs_client.create_general_comment(message)
|
|
89
|
+
|
|
90
|
+
calls = [args for name, args in fake_bitbucket_server_pull_requests_http_client.calls if name == "create_comment"]
|
|
91
|
+
assert len(calls) == 1
|
|
92
|
+
call_args = calls[0]
|
|
93
|
+
assert call_args["text"] == message
|
|
94
|
+
assert call_args["project_key"] == "PRJ"
|
|
95
|
+
assert call_args["repo_slug"] == "repo"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
100
|
+
async def test_create_inline_comment_posts_comment(
|
|
101
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
102
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
103
|
+
):
|
|
104
|
+
"""Should post an inline comment with correct file and line."""
|
|
105
|
+
file = "src/app.py"
|
|
106
|
+
line = 10
|
|
107
|
+
message = "Looks good"
|
|
108
|
+
|
|
109
|
+
await bitbucket_server_vcs_client.create_inline_comment(file, line, message)
|
|
110
|
+
|
|
111
|
+
calls = [args for name, args in fake_bitbucket_server_pull_requests_http_client.calls if name == "create_comment"]
|
|
112
|
+
assert len(calls) == 1
|
|
113
|
+
|
|
114
|
+
call_args = calls[0]
|
|
115
|
+
assert call_args["text"] == message
|
|
116
|
+
assert call_args["anchor"]["path"] == file
|
|
117
|
+
assert call_args["anchor"]["line"] == line
|
|
118
|
+
assert call_args["anchor"]["lineType"] == "ADDED"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
123
|
+
async def test_create_inline_reply_posts_comment(
|
|
124
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
125
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
126
|
+
):
|
|
127
|
+
"""Should post a reply to an existing inline thread."""
|
|
128
|
+
thread_id = 42
|
|
129
|
+
message = "Reply inline comment"
|
|
130
|
+
|
|
131
|
+
await bitbucket_server_vcs_client.create_inline_reply(thread_id, message)
|
|
132
|
+
|
|
133
|
+
calls = [args for name, args in fake_bitbucket_server_pull_requests_http_client.calls if name == "create_comment"]
|
|
134
|
+
assert len(calls) == 1
|
|
135
|
+
|
|
136
|
+
call_args = calls[0]
|
|
137
|
+
assert call_args["parent"]["id"] == thread_id
|
|
138
|
+
assert call_args["text"] == message
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
143
|
+
async def test_create_summary_reply_posts_comment(
|
|
144
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
145
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
146
|
+
):
|
|
147
|
+
"""Should post a reply to a general thread (same API with parent id)."""
|
|
148
|
+
thread_id = 7
|
|
149
|
+
message = "Thanks for the clarification."
|
|
150
|
+
|
|
151
|
+
await bitbucket_server_vcs_client.create_summary_reply(thread_id, message)
|
|
152
|
+
|
|
153
|
+
calls = [args for name, args in fake_bitbucket_server_pull_requests_http_client.calls if name == "create_comment"]
|
|
154
|
+
assert len(calls) == 1
|
|
155
|
+
|
|
156
|
+
call_args = calls[0]
|
|
157
|
+
assert call_args["parent"]["id"] == thread_id
|
|
158
|
+
assert call_args["text"] == message
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.mark.asyncio
|
|
162
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
163
|
+
async def test_get_inline_threads_groups_by_thread_id(
|
|
164
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
165
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
166
|
+
):
|
|
167
|
+
"""Should group inline comments into threads."""
|
|
168
|
+
threads = await bitbucket_server_vcs_client.get_inline_threads()
|
|
169
|
+
|
|
170
|
+
assert all(isinstance(thread, ReviewThreadSchema) for thread in threads)
|
|
171
|
+
assert len(threads) == 1
|
|
172
|
+
|
|
173
|
+
thread = threads[0]
|
|
174
|
+
assert thread.kind == ThreadKind.INLINE
|
|
175
|
+
assert thread.file == "src/main.py"
|
|
176
|
+
assert thread.line == 5
|
|
177
|
+
assert len(thread.comments) == 1
|
|
178
|
+
assert isinstance(thread.comments[0], ReviewCommentSchema)
|
|
179
|
+
|
|
180
|
+
called_methods = [name for name, _ in fake_bitbucket_server_pull_requests_http_client.calls]
|
|
181
|
+
assert "get_comments" in called_methods
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.asyncio
|
|
185
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
186
|
+
async def test_get_general_threads_groups_by_thread_id(
|
|
187
|
+
bitbucket_server_vcs_client: BitbucketServerVCSClient,
|
|
188
|
+
fake_bitbucket_server_pull_requests_http_client: FakeBitbucketServerPullRequestsHTTPClient,
|
|
189
|
+
):
|
|
190
|
+
"""Should group general (non-inline) comments into SUMMARY threads."""
|
|
191
|
+
threads = await bitbucket_server_vcs_client.get_general_threads()
|
|
192
|
+
|
|
193
|
+
assert all(isinstance(thread, ReviewThreadSchema) for thread in threads)
|
|
194
|
+
assert len(threads) == 1
|
|
195
|
+
thread = threads[0]
|
|
196
|
+
assert thread.kind == ThreadKind.SUMMARY
|
|
197
|
+
assert len(thread.comments) == 1
|
|
198
|
+
assert isinstance(thread.comments[0], ReviewCommentSchema)
|
|
199
|
+
|
|
200
|
+
called_methods = [name for name, _ in fake_bitbucket_server_pull_requests_http_client.calls]
|
|
201
|
+
assert "get_comments" in called_methods
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
-
from ai_review.services.vcs.
|
|
3
|
+
from ai_review.services.vcs.bitbucket_cloud.client import BitbucketCloudVCSClient
|
|
4
|
+
from ai_review.services.vcs.bitbucket_server.client import BitbucketServerVCSClient
|
|
4
5
|
from ai_review.services.vcs.factory import get_vcs_client
|
|
5
6
|
from ai_review.services.vcs.gitea.client import GiteaVCSClient
|
|
6
7
|
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
@@ -25,10 +26,16 @@ def test_get_vcs_client_returns_gitlab(monkeypatch: pytest.MonkeyPatch):
|
|
|
25
26
|
assert isinstance(client, GitLabVCSClient)
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
@pytest.mark.usefixtures("
|
|
29
|
-
def
|
|
29
|
+
@pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
|
|
30
|
+
def test_get_vcs_client_returns_bitbucket_cloud(monkeypatch: pytest.MonkeyPatch):
|
|
30
31
|
client = get_vcs_client()
|
|
31
|
-
assert isinstance(client,
|
|
32
|
+
assert isinstance(client, BitbucketCloudVCSClient)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.usefixtures("bitbucket_server_http_client_config")
|
|
36
|
+
def test_get_vcs_client_returns_bitbucket_server(monkeypatch: pytest.MonkeyPatch):
|
|
37
|
+
client = get_vcs_client()
|
|
38
|
+
assert isinstance(client, BitbucketServerVCSClient)
|
|
32
39
|
|
|
33
40
|
|
|
34
41
|
def test_get_vcs_client_unsupported_provider(monkeypatch: pytest.MonkeyPatch):
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xai-review
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: AI-powered code review tool for GitHub, GitLab, Bitbucket and Gitea — built with LLMs like OpenAI, Claude, Gemini, Ollama, and OpenRouter
|
|
5
|
-
Author-email: Nikita Filonov <nikita.filonov@
|
|
3
|
+
Version: 0.39.0
|
|
4
|
+
Summary: AI-powered code review tool for GitHub, GitLab, Bitbucket Cloud, Bitbucket Server and Gitea — built with LLMs like OpenAI, Claude, Gemini, Ollama, and OpenRouter
|
|
5
|
+
Author-email: Nikita Filonov <nikita.filonov@gmail.com>
|
|
6
6
|
Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
|
|
7
7
|
License: Apache License
|
|
8
8
|
Version 2.0, January 2004
|
|
@@ -208,7 +208,7 @@ License: Apache License
|
|
|
208
208
|
Project-URL: Issues, https://github.com/Nikita-Filonov/ai-review/issues
|
|
209
209
|
Project-URL: Homepage, https://github.com/Nikita-Filonov/ai-review
|
|
210
210
|
Project-URL: Repository, https://github.com/Nikita-Filonov/ai-review
|
|
211
|
-
Keywords: ai,code review,llm,openai,claude,gemini,ollama,openrouter,ci/cd,gitlab,github,gitea,bitbucket
|
|
211
|
+
Keywords: ai,code review,llm,openai,claude,gemini,ollama,openrouter,ci/cd,gitlab,github,gitea,bitbucket,bitbucket cloud,bitbucket server
|
|
212
212
|
Classifier: Programming Language :: Python :: 3
|
|
213
213
|
Classifier: Programming Language :: Python :: 3.11
|
|
214
214
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -268,7 +268,8 @@ improve code quality, enforce consistency, and speed up the review process.
|
|
|
268
268
|
|
|
269
269
|
- **Multiple LLM providers** — choose between **OpenAI**, **Claude**, **Gemini**, **Ollama**, or **OpenRouter**, and
|
|
270
270
|
switch anytime.
|
|
271
|
-
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket**,
|
|
271
|
+
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket Cloud**, **Bitbucket Server**,
|
|
272
|
+
and **Gitea**.
|
|
272
273
|
- **Customizable prompts** — adapt inline, context, and summary reviews to match your team’s coding guidelines.
|
|
273
274
|
- **Reply modes** — AI can now **participate in existing review threads**, adding follow-up replies in both inline and
|
|
274
275
|
summary discussions.
|
|
@@ -379,7 +380,8 @@ Key things you can customize:
|
|
|
379
380
|
|
|
380
381
|
- **LLM provider** — OpenAI, Gemini, Claude, Ollama, or OpenRouter
|
|
381
382
|
- **Model settings** — model name, temperature, max tokens
|
|
382
|
-
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket**, and
|
|
383
|
+
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket Cloud**, **Bitbucket Server**, and
|
|
384
|
+
**Gitea**
|
|
383
385
|
- **Review policy** — which files to include/exclude, review modes
|
|
384
386
|
- **Prompts** — inline/context/summary prompt templates
|
|
385
387
|
|
|
@@ -421,7 +423,7 @@ jobs:
|
|
|
421
423
|
with:
|
|
422
424
|
fetch-depth: 0
|
|
423
425
|
|
|
424
|
-
- uses: Nikita-Filonov/ai-review@v0.
|
|
426
|
+
- uses: Nikita-Filonov/ai-review@v0.39.0
|
|
425
427
|
with:
|
|
426
428
|
review-command: ${{ inputs.review-command }}
|
|
427
429
|
env:
|