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.

Files changed (62) hide show
  1. ai_review/clients/{bitbucket → bitbucket_cloud}/client.py +6 -6
  2. ai_review/clients/{bitbucket → bitbucket_cloud}/pr/client.py +52 -40
  3. ai_review/clients/bitbucket_cloud/pr/schema/comments.py +59 -0
  4. ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/files.py +7 -7
  5. ai_review/clients/bitbucket_cloud/pr/schema/pull_request.py +34 -0
  6. ai_review/clients/bitbucket_cloud/pr/schema/user.py +7 -0
  7. ai_review/clients/bitbucket_cloud/pr/types.py +44 -0
  8. ai_review/clients/{bitbucket → bitbucket_cloud}/tools.py +1 -1
  9. ai_review/clients/bitbucket_server/client.py +32 -0
  10. ai_review/clients/bitbucket_server/pr/client.py +163 -0
  11. ai_review/clients/bitbucket_server/pr/schema/changes.py +36 -0
  12. ai_review/clients/bitbucket_server/pr/schema/comments.py +55 -0
  13. ai_review/clients/bitbucket_server/pr/schema/pull_request.py +48 -0
  14. ai_review/clients/bitbucket_server/pr/schema/user.py +13 -0
  15. ai_review/clients/bitbucket_server/pr/types.py +44 -0
  16. ai_review/clients/bitbucket_server/tools.py +6 -0
  17. ai_review/libs/config/vcs/base.py +23 -6
  18. ai_review/libs/config/vcs/{bitbucket.py → bitbucket_cloud.py} +2 -2
  19. ai_review/libs/config/vcs/bitbucket_server.py +13 -0
  20. ai_review/libs/constants/vcs_provider.py +2 -1
  21. ai_review/libs/http/client.py +1 -1
  22. ai_review/services/vcs/bitbucket_cloud/__init__.py +0 -0
  23. ai_review/services/vcs/{bitbucket → bitbucket_cloud}/adapter.py +4 -4
  24. ai_review/services/vcs/{bitbucket → bitbucket_cloud}/client.py +26 -23
  25. ai_review/services/vcs/bitbucket_server/__init__.py +0 -0
  26. ai_review/services/vcs/bitbucket_server/adapter.py +27 -0
  27. ai_review/services/vcs/bitbucket_server/client.py +263 -0
  28. ai_review/services/vcs/factory.py +6 -3
  29. ai_review/tests/fixtures/clients/bitbucket_cloud.py +207 -0
  30. ai_review/tests/fixtures/clients/bitbucket_server.py +265 -0
  31. ai_review/tests/suites/clients/bitbucket_cloud/__init__.py +0 -0
  32. ai_review/tests/suites/clients/bitbucket_cloud/test_client.py +14 -0
  33. ai_review/tests/suites/clients/bitbucket_cloud/test_tools.py +31 -0
  34. ai_review/tests/suites/clients/bitbucket_server/__init__.py +0 -0
  35. ai_review/tests/suites/clients/bitbucket_server/test_client.py +14 -0
  36. ai_review/tests/suites/clients/bitbucket_server/test_tools.py +38 -0
  37. ai_review/tests/suites/services/vcs/bitbucket_cloud/__init__.py +0 -0
  38. ai_review/tests/suites/services/vcs/{bitbucket → bitbucket_cloud}/test_adapter.py +24 -24
  39. ai_review/tests/suites/services/vcs/{bitbucket → bitbucket_cloud}/test_client.py +51 -51
  40. ai_review/tests/suites/services/vcs/bitbucket_server/__init__.py +0 -0
  41. ai_review/tests/suites/services/vcs/bitbucket_server/test_adapter.py +115 -0
  42. ai_review/tests/suites/services/vcs/bitbucket_server/test_client.py +201 -0
  43. ai_review/tests/suites/services/vcs/test_factory.py +11 -4
  44. {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/METADATA +9 -7
  45. {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/RECORD +55 -33
  46. ai_review/clients/bitbucket/pr/schema/comments.py +0 -63
  47. ai_review/clients/bitbucket/pr/schema/pull_request.py +0 -34
  48. ai_review/clients/bitbucket/pr/schema/user.py +0 -7
  49. ai_review/clients/bitbucket/pr/types.py +0 -44
  50. ai_review/tests/fixtures/clients/bitbucket.py +0 -204
  51. ai_review/tests/suites/clients/bitbucket/test_client.py +0 -14
  52. ai_review/tests/suites/clients/bitbucket/test_tools.py +0 -31
  53. /ai_review/clients/{bitbucket → bitbucket_cloud}/__init__.py +0 -0
  54. /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/__init__.py +0 -0
  55. /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/__init__.py +0 -0
  56. /ai_review/{services/vcs/bitbucket → clients/bitbucket_server}/__init__.py +0 -0
  57. /ai_review/{tests/suites/clients/bitbucket → clients/bitbucket_server/pr}/__init__.py +0 -0
  58. /ai_review/{tests/suites/services/vcs/bitbucket → clients/bitbucket_server/pr/schema}/__init__.py +0 -0
  59. {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/WHEEL +0 -0
  60. {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/entry_points.txt +0 -0
  61. {xai_review-0.37.0.dist-info → xai_review-0.39.0.dist-info}/licenses/LICENSE +0 -0
  62. {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.bitbucket.client import BitbucketVCSClient
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.bitbucket import FakeBitbucketPullRequestsHTTPClient
5
+ from ai_review.tests.fixtures.clients.bitbucket_cloud import FakeBitbucketCloudPullRequestsHTTPClient
6
6
 
7
7
 
8
8
  @pytest.mark.asyncio
9
- @pytest.mark.usefixtures("bitbucket_http_client_config")
9
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
10
10
  async def test_get_review_info_returns_valid_schema(
11
- bitbucket_vcs_client: BitbucketVCSClient,
12
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.get_review_info()
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 {r.username for r in info.reviewers} == {"reviewer"}
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 fake_bitbucket_pull_requests_http_client.calls]
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("bitbucket_http_client_config")
38
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
39
39
  async def test_get_general_comments_filters_inline(
40
- bitbucket_vcs_client: BitbucketVCSClient,
41
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.get_general_comments()
44
+ comments = await bitbucket_cloud_vcs_client.get_general_comments()
45
45
 
46
- assert all(isinstance(c, ReviewCommentSchema) for c in comments)
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 fake_bitbucket_pull_requests_http_client.calls]
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("bitbucket_http_client_config")
59
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
60
60
  async def test_get_inline_comments_filters_general(
61
- bitbucket_vcs_client: BitbucketVCSClient,
62
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.get_inline_comments()
65
+ comments = await bitbucket_cloud_vcs_client.get_inline_comments()
66
66
 
67
- assert all(isinstance(c, ReviewCommentSchema) for c in comments)
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 fake_bitbucket_pull_requests_http_client.calls]
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("bitbucket_http_client_config")
80
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
81
81
  async def test_create_general_comment_posts_comment(
82
- bitbucket_vcs_client: BitbucketVCSClient,
83
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.create_general_comment(message)
88
+ await bitbucket_cloud_vcs_client.create_general_comment(message)
89
89
 
90
- calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
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("bitbucket_http_client_config")
99
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
100
100
  async def test_create_inline_comment_posts_comment(
101
- bitbucket_vcs_client: BitbucketVCSClient,
102
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.create_inline_comment(file, line, message)
109
+ await bitbucket_cloud_vcs_client.create_inline_comment(file, line, message)
110
110
 
111
- calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
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("bitbucket_http_client_config")
121
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
122
122
  async def test_create_inline_reply_posts_comment(
123
- bitbucket_vcs_client: BitbucketVCSClient,
124
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.create_inline_reply(thread_id, message)
130
+ await bitbucket_cloud_vcs_client.create_inline_reply(thread_id, message)
131
131
 
132
- calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
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("bitbucket_http_client_config")
143
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
144
144
  async def test_create_summary_reply_posts_comment_with_parent(
145
- bitbucket_vcs_client: BitbucketVCSClient,
146
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.create_summary_reply(thread_id, message)
152
+ await bitbucket_cloud_vcs_client.create_summary_reply(thread_id, message)
153
153
 
154
- calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
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("bitbucket_http_client_config")
165
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
166
166
  async def test_get_inline_threads_groups_by_thread_id(
167
- bitbucket_vcs_client: BitbucketVCSClient,
168
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.get_inline_threads()
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 fake_bitbucket_pull_requests_http_client.calls]
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("bitbucket_http_client_config")
188
+ @pytest.mark.usefixtures("bitbucket_cloud_http_client_config")
189
189
  async def test_get_general_threads_groups_by_thread_id(
190
- bitbucket_vcs_client: BitbucketVCSClient,
191
- fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
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 bitbucket_vcs_client.get_general_threads()
194
+ threads = await bitbucket_cloud_vcs_client.get_general_threads()
195
195
 
196
- assert all(isinstance(t, ReviewThreadSchema) for t in threads)
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 fake_bitbucket_pull_requests_http_client.calls]
203
+ called_methods = [name for name, _ in fake_bitbucket_cloud_pull_requests_http_client.calls]
204
204
  assert "get_comments" in called_methods
@@ -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.bitbucket.client import BitbucketVCSClient
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("bitbucket_http_client_config")
29
- def test_get_vcs_client_returns_bitbucket(monkeypatch: pytest.MonkeyPatch):
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, BitbucketVCSClient)
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.37.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@example.com>
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**, and **Gitea**.
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 **Gitea**
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.37.0
426
+ - uses: Nikita-Filonov/ai-review@v0.39.0
425
427
  with:
426
428
  review-command: ${{ inputs.review-command }}
427
429
  env: