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.
- ai_review/clients/{bitbucket → bitbucket_cloud}/client.py +6 -6
- ai_review/clients/{bitbucket → bitbucket_cloud}/pr/client.py +51 -39
- 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 → bitbucket_cloud}/pr/schema/user.py +1 -1
- 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 +2 -2
- ai_review/services/vcs/{bitbucket → bitbucket_cloud}/client.py +24 -21
- 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.38.0.dist-info}/METADATA +9 -7
- {xai_review-0.37.0.dist-info → xai_review-0.38.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/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.38.0.dist-info}/WHEEL +0 -0
- {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
8
|
-
from ai_review.services.vcs.
|
|
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 =
|
|
14
|
+
comment = BitbucketCloudPRCommentSchema(
|
|
15
15
|
id=101,
|
|
16
|
-
user=
|
|
16
|
+
user=BitbucketCloudUserSchema(uuid="u-123", display_name="Alice", nickname="alice"),
|
|
17
17
|
parent=None,
|
|
18
|
-
inline=
|
|
19
|
-
content=
|
|
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 =
|
|
40
|
+
comment = BitbucketCloudPRCommentSchema(
|
|
41
41
|
id=202,
|
|
42
|
-
user=
|
|
43
|
-
parent=
|
|
44
|
-
inline=
|
|
45
|
-
content=
|
|
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 =
|
|
59
|
+
comment = BitbucketCloudPRCommentSchema(
|
|
60
60
|
id=303,
|
|
61
61
|
user=None,
|
|
62
62
|
parent=None,
|
|
63
|
-
inline=
|
|
64
|
-
content=
|
|
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 =
|
|
77
|
+
comment = BitbucketCloudPRCommentSchema(
|
|
78
78
|
id=404,
|
|
79
|
-
user=
|
|
79
|
+
user=BitbucketCloudUserSchema(uuid="u-789", display_name="Charlie", nickname="charlie"),
|
|
80
80
|
parent=None,
|
|
81
81
|
inline=None,
|
|
82
|
-
content=
|
|
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 =
|
|
94
|
+
comment = BitbucketCloudPRCommentSchema(
|
|
95
95
|
id=505,
|
|
96
96
|
user=None,
|
|
97
97
|
parent=None,
|
|
98
98
|
inline=None,
|
|
99
|
-
content=
|
|
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.
|
|
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
|