xai-review 0.37.0__py3-none-any.whl → 0.38.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 (61) hide show
  1. ai_review/clients/{bitbucket → bitbucket_cloud}/client.py +6 -6
  2. ai_review/clients/{bitbucket → bitbucket_cloud}/pr/client.py +51 -39
  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 → bitbucket_cloud}/pr/schema/user.py +1 -1
  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 +2 -2
  24. ai_review/services/vcs/{bitbucket → bitbucket_cloud}/client.py +24 -21
  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.38.0.dist-info}/METADATA +9 -7
  45. {xai_review-0.37.0.dist-info → xai_review-0.38.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/types.py +0 -44
  49. ai_review/tests/fixtures/clients/bitbucket.py +0 -204
  50. ai_review/tests/suites/clients/bitbucket/test_client.py +0 -14
  51. ai_review/tests/suites/clients/bitbucket/test_tools.py +0 -31
  52. /ai_review/clients/{bitbucket → bitbucket_cloud}/__init__.py +0 -0
  53. /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/__init__.py +0 -0
  54. /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/__init__.py +0 -0
  55. /ai_review/{services/vcs/bitbucket → clients/bitbucket_server}/__init__.py +0 -0
  56. /ai_review/{tests/suites/clients/bitbucket → clients/bitbucket_server/pr}/__init__.py +0 -0
  57. /ai_review/{tests/suites/services/vcs/bitbucket → clients/bitbucket_server/pr/schema}/__init__.py +0 -0
  58. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/WHEEL +0 -0
  59. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/entry_points.txt +0 -0
  60. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/licenses/LICENSE +0 -0
  61. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,22 @@
1
- from ai_review.clients.bitbucket.pr.schema.comments import (
2
- BitbucketPRCommentSchema,
3
- BitbucketCommentContentSchema,
4
- BitbucketCommentInlineSchema,
5
- BitbucketCommentParentSchema,
1
+ from ai_review.clients.bitbucket_cloud.pr.schema.comments import (
2
+ BitbucketCloudPRCommentSchema,
3
+ BitbucketCloudCommentContentSchema,
4
+ BitbucketCloudCommentInlineSchema,
5
+ BitbucketCloudCommentParentSchema,
6
6
  )
7
- from ai_review.clients.bitbucket.pr.schema.user import BitbucketUserSchema
8
- from ai_review.services.vcs.bitbucket.adapter import get_review_comment_from_bitbucket_pr_comment
7
+ from ai_review.clients.bitbucket_cloud.pr.schema.user import BitbucketCloudUserSchema
8
+ from ai_review.services.vcs.bitbucket_cloud.adapter import get_review_comment_from_bitbucket_pr_comment
9
9
  from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
10
10
 
11
11
 
12
12
  def test_maps_all_fields_correctly():
13
13
  """Should map Bitbucket PR comment with all fields correctly."""
14
- comment = BitbucketPRCommentSchema(
14
+ comment = BitbucketCloudPRCommentSchema(
15
15
  id=101,
16
- user=BitbucketUserSchema(uuid="u-123", display_name="Alice", nickname="alice"),
16
+ user=BitbucketCloudUserSchema(uuid="u-123", display_name="Alice", nickname="alice"),
17
17
  parent=None,
18
- inline=BitbucketCommentInlineSchema(path="src/utils.py", to_line=10),
19
- content=BitbucketCommentContentSchema(raw="Looks good"),
18
+ inline=BitbucketCloudCommentInlineSchema(path="src/utils.py", to_line=10),
19
+ content=BitbucketCloudCommentContentSchema(raw="Looks good"),
20
20
  )
21
21
 
22
22
  result = get_review_comment_from_bitbucket_pr_comment(comment)
@@ -37,12 +37,12 @@ def test_maps_all_fields_correctly():
37
37
 
38
38
  def test_maps_with_parent_comment():
39
39
  """Should set parent_id and use it as thread_id."""
40
- comment = BitbucketPRCommentSchema(
40
+ comment = BitbucketCloudPRCommentSchema(
41
41
  id=202,
42
- user=BitbucketUserSchema(uuid="u-456", display_name="Bob", nickname="bob"),
43
- parent=BitbucketCommentParentSchema(id=101),
44
- inline=BitbucketCommentInlineSchema(path="src/main.py", to_line=20),
45
- content=BitbucketCommentContentSchema(raw="I agree"),
42
+ user=BitbucketCloudUserSchema(uuid="u-456", display_name="Bob", nickname="bob"),
43
+ parent=BitbucketCloudCommentParentSchema(id=101),
44
+ inline=BitbucketCloudCommentInlineSchema(path="src/main.py", to_line=20),
45
+ content=BitbucketCloudCommentContentSchema(raw="I agree"),
46
46
  )
47
47
 
48
48
  result = get_review_comment_from_bitbucket_pr_comment(comment)
@@ -56,12 +56,12 @@ def test_maps_with_parent_comment():
56
56
 
57
57
  def test_maps_without_user():
58
58
  """Should handle missing user gracefully."""
59
- comment = BitbucketPRCommentSchema(
59
+ comment = BitbucketCloudPRCommentSchema(
60
60
  id=303,
61
61
  user=None,
62
62
  parent=None,
63
- inline=BitbucketCommentInlineSchema(path="src/app.py", to_line=5),
64
- content=BitbucketCommentContentSchema(raw="Anonymous feedback"),
63
+ inline=BitbucketCloudCommentInlineSchema(path="src/app.py", to_line=5),
64
+ content=BitbucketCloudCommentContentSchema(raw="Anonymous feedback"),
65
65
  )
66
66
 
67
67
  result = get_review_comment_from_bitbucket_pr_comment(comment)
@@ -74,12 +74,12 @@ def test_maps_without_user():
74
74
 
75
75
  def test_maps_without_inline():
76
76
  """Should handle missing inline gracefully (file and line None)."""
77
- comment = BitbucketPRCommentSchema(
77
+ comment = BitbucketCloudPRCommentSchema(
78
78
  id=404,
79
- user=BitbucketUserSchema(uuid="u-789", display_name="Charlie", nickname="charlie"),
79
+ user=BitbucketCloudUserSchema(uuid="u-789", display_name="Charlie", nickname="charlie"),
80
80
  parent=None,
81
81
  inline=None,
82
- content=BitbucketCommentContentSchema(raw="General comment"),
82
+ content=BitbucketCloudCommentContentSchema(raw="General comment"),
83
83
  )
84
84
 
85
85
  result = get_review_comment_from_bitbucket_pr_comment(comment)
@@ -91,12 +91,12 @@ def test_maps_without_inline():
91
91
 
92
92
  def test_maps_with_empty_body_and_defaults():
93
93
  """Should default body to empty string if content.raw is empty or None."""
94
- comment = BitbucketPRCommentSchema(
94
+ comment = BitbucketCloudPRCommentSchema(
95
95
  id=505,
96
96
  user=None,
97
97
  parent=None,
98
98
  inline=None,
99
- content=BitbucketCommentContentSchema(raw="", html=None, markup=None),
99
+ content=BitbucketCloudCommentContentSchema(raw="", html=None, markup=None),
100
100
  )
101
101
 
102
102
  result = get_review_comment_from_bitbucket_pr_comment(comment)
@@ -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