xai-review 0.21.0__py3-none-any.whl → 0.23.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/claude/client.py +1 -1
- ai_review/clients/gemini/client.py +1 -1
- ai_review/clients/github/client.py +1 -1
- ai_review/clients/github/pr/client.py +64 -16
- ai_review/clients/github/pr/schema/comments.py +4 -0
- ai_review/clients/github/pr/schema/files.py +4 -0
- ai_review/clients/github/pr/schema/reviews.py +4 -0
- ai_review/clients/github/pr/types.py +49 -0
- ai_review/clients/gitlab/client.py +1 -1
- ai_review/clients/gitlab/mr/client.py +25 -8
- ai_review/clients/gitlab/mr/schema/discussions.py +4 -0
- ai_review/clients/gitlab/mr/schema/notes.py +4 -0
- ai_review/clients/gitlab/mr/types.py +35 -0
- ai_review/clients/openai/client.py +1 -1
- ai_review/config.py +2 -0
- ai_review/libs/asynchronous/gather.py +6 -3
- ai_review/libs/config/core.py +5 -0
- ai_review/libs/http/event_hooks/logger.py +5 -2
- ai_review/libs/http/transports/retry.py +23 -6
- ai_review/services/diff/renderers.py +3 -3
- ai_review/services/prompt/schema.py +13 -24
- ai_review/tests/fixtures/clients/claude.py +22 -0
- ai_review/tests/fixtures/clients/gemini.py +21 -0
- ai_review/tests/fixtures/clients/github.py +181 -0
- ai_review/tests/fixtures/clients/gitlab.py +150 -0
- ai_review/tests/fixtures/clients/openai.py +21 -0
- ai_review/tests/fixtures/services/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/__init__.py +0 -0
- ai_review/tests/suites/clients/claude/test_client.py +1 -20
- ai_review/tests/suites/clients/gemini/test_client.py +1 -19
- ai_review/tests/suites/clients/github/test_client.py +1 -23
- ai_review/tests/suites/clients/gitlab/test_client.py +1 -22
- ai_review/tests/suites/clients/openai/test_client.py +1 -19
- ai_review/tests/suites/libs/asynchronous/__init__.py +0 -0
- ai_review/tests/suites/libs/asynchronous/test_gather.py +46 -0
- ai_review/tests/suites/services/diff/test_service.py +1 -1
- ai_review/tests/suites/services/diff/test_tools.py +1 -1
- ai_review/tests/suites/services/llm/__init__.py +0 -0
- ai_review/tests/suites/services/llm/test_factory.py +30 -0
- ai_review/tests/suites/services/prompt/test_schema.py +65 -0
- ai_review/tests/suites/services/review/test_service.py +9 -9
- ai_review/tests/suites/services/vcs/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/github/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/github/test_service.py +114 -0
- ai_review/tests/suites/services/vcs/gitlab/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/gitlab/test_service.py +123 -0
- ai_review/tests/suites/services/vcs/test_factory.py +23 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/METADATA +2 -2
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/RECORD +63 -43
- /ai_review/tests/fixtures/{review → clients}/__init__.py +0 -0
- /ai_review/tests/fixtures/{artifacts.py → services/artifacts.py} +0 -0
- /ai_review/tests/fixtures/{cost.py → services/cost.py} +0 -0
- /ai_review/tests/fixtures/{diff.py → services/diff.py} +0 -0
- /ai_review/tests/fixtures/{git.py → services/git.py} +0 -0
- /ai_review/tests/fixtures/{llm.py → services/llm.py} +0 -0
- /ai_review/tests/fixtures/{prompt.py → services/prompt.py} +0 -0
- /ai_review/tests/fixtures/{review → services/review}/inline.py +0 -0
- /ai_review/tests/fixtures/{review → services/review}/summary.py +0 -0
- /ai_review/tests/fixtures/{vcs.py → services/vcs.py} +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/WHEEL +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import HttpUrl, SecretStr
|
|
3
|
+
|
|
4
|
+
from ai_review.clients.github.pr.schema.comments import (
|
|
5
|
+
GitHubPRCommentSchema,
|
|
6
|
+
GitHubGetPRCommentsResponseSchema,
|
|
7
|
+
GitHubCreateIssueCommentResponseSchema,
|
|
8
|
+
GitHubCreateReviewCommentRequestSchema,
|
|
9
|
+
GitHubCreateReviewCommentResponseSchema,
|
|
10
|
+
)
|
|
11
|
+
from ai_review.clients.github.pr.schema.files import GitHubGetPRFilesResponseSchema, GitHubPRFileSchema
|
|
12
|
+
from ai_review.clients.github.pr.schema.pull_request import (
|
|
13
|
+
GitHubUserSchema,
|
|
14
|
+
GitHubLabelSchema,
|
|
15
|
+
GitHubBranchSchema,
|
|
16
|
+
GitHubGetPRResponseSchema,
|
|
17
|
+
)
|
|
18
|
+
from ai_review.clients.github.pr.schema.reviews import GitHubGetPRReviewsResponseSchema, GitHubPRReviewSchema
|
|
19
|
+
from ai_review.clients.github.pr.types import GitHubPullRequestsHTTPClientProtocol
|
|
20
|
+
from ai_review.config import settings
|
|
21
|
+
from ai_review.libs.config.github import GitHubPipelineConfig, GitHubHTTPClientConfig
|
|
22
|
+
from ai_review.libs.config.vcs import GitHubVCSConfig
|
|
23
|
+
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
24
|
+
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FakeGitHubPullRequestsHTTPClient(GitHubPullRequestsHTTPClientProtocol):
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.calls: list[tuple[str, dict]] = []
|
|
30
|
+
|
|
31
|
+
async def get_pull_request(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRResponseSchema:
|
|
32
|
+
self.calls.append(("get_pull_request", {"owner": owner, "repo": repo, "pull_number": pull_number}))
|
|
33
|
+
|
|
34
|
+
return GitHubGetPRResponseSchema(
|
|
35
|
+
id=1,
|
|
36
|
+
number=1,
|
|
37
|
+
title="Fake Pull Request",
|
|
38
|
+
body="This is a fake PR for testing",
|
|
39
|
+
user=GitHubUserSchema(id=101, login="tester"),
|
|
40
|
+
labels=[
|
|
41
|
+
GitHubLabelSchema(id=1, name="bugfix"),
|
|
42
|
+
GitHubLabelSchema(id=2, name="backend"),
|
|
43
|
+
],
|
|
44
|
+
assignees=[
|
|
45
|
+
GitHubUserSchema(id=102, login="dev1"),
|
|
46
|
+
GitHubUserSchema(id=103, login="dev2"),
|
|
47
|
+
],
|
|
48
|
+
requested_reviewers=[
|
|
49
|
+
GitHubUserSchema(id=104, login="reviewer"),
|
|
50
|
+
],
|
|
51
|
+
base=GitHubBranchSchema(ref="main", sha="abc123"),
|
|
52
|
+
head=GitHubBranchSchema(ref="feature/test", sha="def456"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def get_files(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRFilesResponseSchema:
|
|
56
|
+
self.calls.append(("get_files", {"owner": owner, "repo": repo, "pull_number": pull_number}))
|
|
57
|
+
|
|
58
|
+
return GitHubGetPRFilesResponseSchema(
|
|
59
|
+
root=[
|
|
60
|
+
GitHubPRFileSchema(
|
|
61
|
+
sha="abc",
|
|
62
|
+
status="modified",
|
|
63
|
+
filename="app/main.py",
|
|
64
|
+
patch="@@ -1,2 +1,2 @@\n- old\n+ new",
|
|
65
|
+
),
|
|
66
|
+
GitHubPRFileSchema(
|
|
67
|
+
sha="def",
|
|
68
|
+
status="added",
|
|
69
|
+
filename="utils/helper.py",
|
|
70
|
+
patch="+ print('Hello')",
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def get_issue_comments(self, owner: str, repo: str, issue_number: str) -> GitHubGetPRCommentsResponseSchema:
|
|
76
|
+
self.calls.append(("get_issue_comments", {"owner": owner, "repo": repo, "issue_number": issue_number}))
|
|
77
|
+
|
|
78
|
+
return GitHubGetPRCommentsResponseSchema(
|
|
79
|
+
root=[
|
|
80
|
+
GitHubPRCommentSchema(id=1, body="General comment"),
|
|
81
|
+
GitHubPRCommentSchema(id=2, body="Another general comment"),
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def get_review_comments(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRCommentsResponseSchema:
|
|
86
|
+
self.calls.append(("get_review_comments", {"owner": owner, "repo": repo, "pull_number": pull_number}))
|
|
87
|
+
|
|
88
|
+
return GitHubGetPRCommentsResponseSchema(
|
|
89
|
+
root=[
|
|
90
|
+
GitHubPRCommentSchema(id=3, body="Inline comment", path="file.py", line=5),
|
|
91
|
+
GitHubPRCommentSchema(id=4, body="Another inline comment", path="utils.py", line=10),
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def get_reviews(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRReviewsResponseSchema:
|
|
96
|
+
self.calls.append(("get_reviews", {"owner": owner, "repo": repo, "pull_number": pull_number}))
|
|
97
|
+
|
|
98
|
+
return GitHubGetPRReviewsResponseSchema(
|
|
99
|
+
root=[
|
|
100
|
+
GitHubPRReviewSchema(id=1, body="Looks good", state="APPROVED"),
|
|
101
|
+
GitHubPRReviewSchema(id=2, body="Needs changes", state="CHANGES_REQUESTED"),
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def create_review_comment(
|
|
106
|
+
self,
|
|
107
|
+
owner: str,
|
|
108
|
+
repo: str,
|
|
109
|
+
pull_number: str,
|
|
110
|
+
request: GitHubCreateReviewCommentRequestSchema,
|
|
111
|
+
) -> GitHubCreateReviewCommentResponseSchema:
|
|
112
|
+
self.calls.append(
|
|
113
|
+
(
|
|
114
|
+
"create_review_comment",
|
|
115
|
+
{"owner": owner, "repo": repo, "pull_number": pull_number, **request.model_dump()}
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return GitHubCreateReviewCommentResponseSchema(id=10, body=request.body)
|
|
119
|
+
|
|
120
|
+
async def create_issue_comment(
|
|
121
|
+
self,
|
|
122
|
+
owner: str,
|
|
123
|
+
repo: str,
|
|
124
|
+
issue_number: str,
|
|
125
|
+
body: str,
|
|
126
|
+
) -> GitHubCreateIssueCommentResponseSchema:
|
|
127
|
+
self.calls.append(
|
|
128
|
+
(
|
|
129
|
+
"create_issue_comment",
|
|
130
|
+
{"owner": owner, "repo": repo, "issue_number": issue_number, "body": body}
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
return GitHubCreateIssueCommentResponseSchema(id=11, body=body)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class FakeGitHubHTTPClient:
|
|
137
|
+
def __init__(self, pull_requests_client: GitHubPullRequestsHTTPClientProtocol):
|
|
138
|
+
self.pr = pull_requests_client
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.fixture
|
|
142
|
+
def fake_github_pull_requests_http_client() -> FakeGitHubPullRequestsHTTPClient:
|
|
143
|
+
return FakeGitHubPullRequestsHTTPClient()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.fixture
|
|
147
|
+
def fake_github_http_client(
|
|
148
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient
|
|
149
|
+
) -> FakeGitHubHTTPClient:
|
|
150
|
+
return FakeGitHubHTTPClient(pull_requests_client=fake_github_pull_requests_http_client)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.fixture
|
|
154
|
+
def github_vcs_client(
|
|
155
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
156
|
+
fake_github_http_client: FakeGitHubHTTPClient
|
|
157
|
+
) -> GitHubVCSClient:
|
|
158
|
+
monkeypatch.setattr(
|
|
159
|
+
"ai_review.services.vcs.github.client.get_github_http_client",
|
|
160
|
+
lambda: fake_github_http_client,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return GitHubVCSClient()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.fixture
|
|
167
|
+
def github_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
168
|
+
fake_config = GitHubVCSConfig(
|
|
169
|
+
provider=VCSProvider.GITHUB,
|
|
170
|
+
pipeline=GitHubPipelineConfig(
|
|
171
|
+
repo="repo",
|
|
172
|
+
owner="owner",
|
|
173
|
+
pull_number="pull_number"
|
|
174
|
+
),
|
|
175
|
+
http_client=GitHubHTTPClientConfig(
|
|
176
|
+
timeout=10,
|
|
177
|
+
api_url=HttpUrl("https://github.com"),
|
|
178
|
+
api_token=SecretStr("fake-token"),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
monkeypatch.setattr(settings, "vcs", fake_config)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import HttpUrl, SecretStr
|
|
3
|
+
|
|
4
|
+
from ai_review.clients.gitlab.mr.schema.changes import (
|
|
5
|
+
GitLabUserSchema,
|
|
6
|
+
GitLabMRChangeSchema,
|
|
7
|
+
GitLabDiffRefsSchema,
|
|
8
|
+
GitLabGetMRChangesResponseSchema,
|
|
9
|
+
)
|
|
10
|
+
from ai_review.clients.gitlab.mr.schema.discussions import (
|
|
11
|
+
GitLabDiscussionSchema,
|
|
12
|
+
GitLabGetMRDiscussionsResponseSchema,
|
|
13
|
+
GitLabCreateMRDiscussionRequestSchema,
|
|
14
|
+
GitLabCreateMRDiscussionResponseSchema,
|
|
15
|
+
)
|
|
16
|
+
from ai_review.clients.gitlab.mr.schema.notes import (
|
|
17
|
+
GitLabNoteSchema,
|
|
18
|
+
GitLabGetMRNotesResponseSchema,
|
|
19
|
+
GitLabCreateMRNoteResponseSchema,
|
|
20
|
+
)
|
|
21
|
+
from ai_review.clients.gitlab.mr.types import GitLabMergeRequestsHTTPClientProtocol
|
|
22
|
+
from ai_review.config import settings
|
|
23
|
+
from ai_review.libs.config.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
|
|
24
|
+
from ai_review.libs.config.vcs import GitLabVCSConfig
|
|
25
|
+
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
26
|
+
from ai_review.services.vcs.gitlab.client import GitLabVCSClient
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FakeGitLabMergeRequestsHTTPClient(GitLabMergeRequestsHTTPClientProtocol):
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.calls: list[tuple[str, dict]] = []
|
|
32
|
+
|
|
33
|
+
async def get_changes(self, project_id: str, merge_request_id: str) -> GitLabGetMRChangesResponseSchema:
|
|
34
|
+
self.calls.append(("get_changes", {"project_id": project_id, "merge_request_id": merge_request_id}))
|
|
35
|
+
return GitLabGetMRChangesResponseSchema(
|
|
36
|
+
id=1,
|
|
37
|
+
iid=1,
|
|
38
|
+
title="Fake Merge Request",
|
|
39
|
+
author=GitLabUserSchema(id=42, name="Tester", username="tester"),
|
|
40
|
+
labels=["bugfix", "backend"],
|
|
41
|
+
description="This is a fake MR for testing",
|
|
42
|
+
project_id=1,
|
|
43
|
+
diff_refs=GitLabDiffRefsSchema(
|
|
44
|
+
base_sha="abc123",
|
|
45
|
+
head_sha="def456",
|
|
46
|
+
start_sha="ghi789",
|
|
47
|
+
),
|
|
48
|
+
source_branch="feature/test",
|
|
49
|
+
target_branch="main",
|
|
50
|
+
changes=[
|
|
51
|
+
GitLabMRChangeSchema(
|
|
52
|
+
diff="@@ -1,2 +1,2 @@\n- old\n+ new",
|
|
53
|
+
old_path="main.py",
|
|
54
|
+
new_path="main.py",
|
|
55
|
+
)
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
async def get_notes(self, project_id: str, merge_request_id: str) -> GitLabGetMRNotesResponseSchema:
|
|
60
|
+
self.calls.append(("get_notes", {"project_id": project_id, "merge_request_id": merge_request_id}))
|
|
61
|
+
return GitLabGetMRNotesResponseSchema(
|
|
62
|
+
root=[
|
|
63
|
+
GitLabNoteSchema(id=1, body="General comment"),
|
|
64
|
+
GitLabNoteSchema(id=2, body="Another note"),
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def get_discussions(self, project_id: str, merge_request_id: str) -> GitLabGetMRDiscussionsResponseSchema:
|
|
69
|
+
self.calls.append(("get_discussions", {"project_id": project_id, "merge_request_id": merge_request_id}))
|
|
70
|
+
return GitLabGetMRDiscussionsResponseSchema(
|
|
71
|
+
root=[
|
|
72
|
+
GitLabDiscussionSchema(
|
|
73
|
+
id="discussion-1",
|
|
74
|
+
notes=[
|
|
75
|
+
GitLabNoteSchema(id=10, body="Inline comment A"),
|
|
76
|
+
GitLabNoteSchema(id=11, body="Inline comment B"),
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def create_note(self, body: str, project_id: str, merge_request_id: str) -> GitLabCreateMRNoteResponseSchema:
|
|
83
|
+
self.calls.append(
|
|
84
|
+
(
|
|
85
|
+
"create_note",
|
|
86
|
+
{"body": body, "project_id": project_id, "merge_request_id": merge_request_id}
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return GitLabCreateMRNoteResponseSchema(id=99, body=body)
|
|
90
|
+
|
|
91
|
+
async def create_discussion(
|
|
92
|
+
self,
|
|
93
|
+
project_id: str,
|
|
94
|
+
merge_request_id: str,
|
|
95
|
+
request: GitLabCreateMRDiscussionRequestSchema,
|
|
96
|
+
) -> GitLabCreateMRDiscussionResponseSchema:
|
|
97
|
+
self.calls.append(
|
|
98
|
+
(
|
|
99
|
+
"create_discussion",
|
|
100
|
+
{"project_id": project_id, "merge_request_id": merge_request_id, "body": request.body}
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
return GitLabCreateMRDiscussionResponseSchema(id="discussion-new", body=request.body)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FakeGitLabHTTPClient:
|
|
107
|
+
def __init__(self, merge_requests_client: FakeGitLabMergeRequestsHTTPClient):
|
|
108
|
+
self.mr = merge_requests_client
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.fixture
|
|
112
|
+
def fake_gitlab_merge_requests_http_client() -> FakeGitLabMergeRequestsHTTPClient:
|
|
113
|
+
return FakeGitLabMergeRequestsHTTPClient()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.fixture
|
|
117
|
+
def fake_gitlab_http_client(
|
|
118
|
+
fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient
|
|
119
|
+
) -> FakeGitLabHTTPClient:
|
|
120
|
+
return FakeGitLabHTTPClient(merge_requests_client=fake_gitlab_merge_requests_http_client)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.fixture
|
|
124
|
+
def gitlab_vcs_client(
|
|
125
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
126
|
+
fake_gitlab_http_client: FakeGitLabHTTPClient
|
|
127
|
+
) -> GitLabVCSClient:
|
|
128
|
+
monkeypatch.setattr(
|
|
129
|
+
"ai_review.services.vcs.gitlab.client.get_gitlab_http_client",
|
|
130
|
+
lambda: fake_gitlab_http_client,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return GitLabVCSClient()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.fixture
|
|
137
|
+
def gitlab_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
138
|
+
fake_config = GitLabVCSConfig(
|
|
139
|
+
provider=VCSProvider.GITLAB,
|
|
140
|
+
pipeline=GitLabPipelineConfig(
|
|
141
|
+
project_id="project-id",
|
|
142
|
+
merge_request_id="merge-request-id"
|
|
143
|
+
),
|
|
144
|
+
http_client=GitLabHTTPClientConfig(
|
|
145
|
+
timeout=10,
|
|
146
|
+
api_url=HttpUrl("https://gitlab.com"),
|
|
147
|
+
api_token=SecretStr("fake-token"),
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
monkeypatch.setattr(settings, "vcs", fake_config)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import HttpUrl, SecretStr
|
|
3
|
+
|
|
4
|
+
from ai_review.config import settings
|
|
5
|
+
from ai_review.libs.config.llm import OpenAILLMConfig
|
|
6
|
+
from ai_review.libs.config.openai import OpenAIMetaConfig, OpenAIHTTPClientConfig
|
|
7
|
+
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def openai_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
12
|
+
fake_config = OpenAILLMConfig(
|
|
13
|
+
meta=OpenAIMetaConfig(),
|
|
14
|
+
provider=LLMProvider.OPENAI,
|
|
15
|
+
http_client=OpenAIHTTPClientConfig(
|
|
16
|
+
timeout=10,
|
|
17
|
+
api_url=HttpUrl("https://api.openai.com/v1"),
|
|
18
|
+
api_token=SecretStr("fake-token"),
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
monkeypatch.setattr(settings, "llm", fake_config)
|
|
File without changes
|
|
File without changes
|
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from httpx import AsyncClient
|
|
3
|
-
from pydantic import HttpUrl, SecretStr
|
|
4
3
|
|
|
5
4
|
from ai_review.clients.claude.client import get_claude_http_client, ClaudeHTTPClient
|
|
6
|
-
from ai_review.config import settings
|
|
7
|
-
from ai_review.libs.config.claude import ClaudeMetaConfig
|
|
8
|
-
from ai_review.libs.config.llm import ClaudeLLMConfig, ClaudeHTTPClientConfig
|
|
9
|
-
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture(autouse=True)
|
|
13
|
-
def claude_http_client_config(monkeypatch):
|
|
14
|
-
fake_config = ClaudeLLMConfig(
|
|
15
|
-
meta=ClaudeMetaConfig(),
|
|
16
|
-
provider=LLMProvider.CLAUDE,
|
|
17
|
-
http_client=ClaudeHTTPClientConfig(
|
|
18
|
-
timeout=10,
|
|
19
|
-
api_url=HttpUrl("https://api.anthropic.com"),
|
|
20
|
-
api_token=SecretStr("fake-token"),
|
|
21
|
-
api_version="2023-06-01",
|
|
22
|
-
)
|
|
23
|
-
)
|
|
24
|
-
monkeypatch.setattr(settings, "llm", fake_config)
|
|
25
5
|
|
|
26
6
|
|
|
7
|
+
@pytest.mark.usefixtures('claude_http_client_config')
|
|
27
8
|
def test_get_claude_http_client_builds_ok():
|
|
28
9
|
claude_http_client = get_claude_http_client()
|
|
29
10
|
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from httpx import AsyncClient
|
|
3
|
-
from pydantic import HttpUrl, SecretStr
|
|
4
3
|
|
|
5
4
|
from ai_review.clients.gemini.client import get_gemini_http_client, GeminiHTTPClient
|
|
6
|
-
from ai_review.config import settings
|
|
7
|
-
from ai_review.libs.config.gemini import GeminiMetaConfig, GeminiHTTPClientConfig
|
|
8
|
-
from ai_review.libs.config.llm import GeminiLLMConfig
|
|
9
|
-
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture(autouse=True)
|
|
13
|
-
def gemini_http_client_config(monkeypatch):
|
|
14
|
-
fake_config = GeminiLLMConfig(
|
|
15
|
-
meta=GeminiMetaConfig(),
|
|
16
|
-
provider=LLMProvider.GEMINI,
|
|
17
|
-
http_client=GeminiHTTPClientConfig(
|
|
18
|
-
timeout=10,
|
|
19
|
-
api_url=HttpUrl("https://generativelanguage.googleapis.com"),
|
|
20
|
-
api_token=SecretStr("fake-token"),
|
|
21
|
-
)
|
|
22
|
-
)
|
|
23
|
-
monkeypatch.setattr(settings, "llm", fake_config)
|
|
24
5
|
|
|
25
6
|
|
|
7
|
+
@pytest.mark.usefixtures('gemini_http_client_config')
|
|
26
8
|
def test_get_gemini_http_client_builds_ok():
|
|
27
9
|
gemini_http_client = get_gemini_http_client()
|
|
28
10
|
|
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from httpx import AsyncClient
|
|
3
|
-
from pydantic import HttpUrl, SecretStr
|
|
4
3
|
|
|
5
4
|
from ai_review.clients.github.client import get_github_http_client, GitHubHTTPClient
|
|
6
5
|
from ai_review.clients.github.pr.client import GitHubPullRequestsHTTPClient
|
|
7
|
-
from ai_review.config import settings
|
|
8
|
-
from ai_review.libs.config.github import GitHubPipelineConfig, GitHubHTTPClientConfig
|
|
9
|
-
from ai_review.libs.config.vcs import GitHubVCSConfig
|
|
10
|
-
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.fixture(autouse=True)
|
|
14
|
-
def github_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
15
|
-
fake_config = GitHubVCSConfig(
|
|
16
|
-
provider=VCSProvider.GITHUB,
|
|
17
|
-
pipeline=GitHubPipelineConfig(
|
|
18
|
-
repo="repo",
|
|
19
|
-
owner="owner",
|
|
20
|
-
pull_number="pull_number"
|
|
21
|
-
),
|
|
22
|
-
http_client=GitHubHTTPClientConfig(
|
|
23
|
-
timeout=10,
|
|
24
|
-
api_url=HttpUrl("https://github.com"),
|
|
25
|
-
api_token=SecretStr("fake-token"),
|
|
26
|
-
)
|
|
27
|
-
)
|
|
28
|
-
monkeypatch.setattr(settings, "vcs", fake_config)
|
|
29
6
|
|
|
30
7
|
|
|
8
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
31
9
|
def test_get_github_http_client_builds_ok():
|
|
32
10
|
github_http_client = get_github_http_client()
|
|
33
11
|
|
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from httpx import AsyncClient
|
|
3
|
-
from pydantic import HttpUrl, SecretStr
|
|
4
3
|
|
|
5
4
|
from ai_review.clients.gitlab.client import get_gitlab_http_client, GitLabHTTPClient
|
|
6
5
|
from ai_review.clients.gitlab.mr.client import GitLabMergeRequestsHTTPClient
|
|
7
|
-
from ai_review.config import settings
|
|
8
|
-
from ai_review.libs.config.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
|
|
9
|
-
from ai_review.libs.config.vcs import GitLabVCSConfig
|
|
10
|
-
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.fixture(autouse=True)
|
|
14
|
-
def gitlab_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
15
|
-
fake_config = GitLabVCSConfig(
|
|
16
|
-
provider=VCSProvider.GITLAB,
|
|
17
|
-
pipeline=GitLabPipelineConfig(
|
|
18
|
-
project_id="project-id",
|
|
19
|
-
merge_request_id="merge-request-id"
|
|
20
|
-
),
|
|
21
|
-
http_client=GitLabHTTPClientConfig(
|
|
22
|
-
timeout=10,
|
|
23
|
-
api_url=HttpUrl("https://gitlab.com"),
|
|
24
|
-
api_token=SecretStr("fake-token"),
|
|
25
|
-
)
|
|
26
|
-
)
|
|
27
|
-
monkeypatch.setattr(settings, "vcs", fake_config)
|
|
28
6
|
|
|
29
7
|
|
|
8
|
+
@pytest.mark.usefixtures("gitlab_http_client_config")
|
|
30
9
|
def test_get_gitlab_http_client_builds_ok():
|
|
31
10
|
gitlab_http_client = get_gitlab_http_client()
|
|
32
11
|
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from httpx import AsyncClient
|
|
3
|
-
from pydantic import HttpUrl, SecretStr
|
|
4
3
|
|
|
5
4
|
from ai_review.clients.openai.client import get_openai_http_client, OpenAIHTTPClient
|
|
6
|
-
from ai_review.config import settings
|
|
7
|
-
from ai_review.libs.config.llm import OpenAILLMConfig
|
|
8
|
-
from ai_review.libs.config.openai import OpenAIMetaConfig, OpenAIHTTPClientConfig
|
|
9
|
-
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture(autouse=True)
|
|
13
|
-
def openai_http_client_config(monkeypatch):
|
|
14
|
-
fake_config = OpenAILLMConfig(
|
|
15
|
-
meta=OpenAIMetaConfig(),
|
|
16
|
-
provider=LLMProvider.OPENAI,
|
|
17
|
-
http_client=OpenAIHTTPClientConfig(
|
|
18
|
-
timeout=10,
|
|
19
|
-
api_url=HttpUrl("https://api.openai.com/v1"),
|
|
20
|
-
api_token=SecretStr("fake-token"),
|
|
21
|
-
)
|
|
22
|
-
)
|
|
23
|
-
monkeypatch.setattr(settings, "llm", fake_config)
|
|
24
5
|
|
|
25
6
|
|
|
7
|
+
@pytest.mark.usefixtures('openai_http_client_config')
|
|
26
8
|
def test_get_openai_http_client_builds_ok():
|
|
27
9
|
openai_http_client = get_openai_http_client()
|
|
28
10
|
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from ai_review.config import settings
|
|
6
|
+
from ai_review.libs.asynchronous.gather import bounded_gather
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_bounded_gather_limits_concurrency(monkeypatch: pytest.MonkeyPatch):
|
|
11
|
+
concurrency_limit = 3
|
|
12
|
+
monkeypatch.setattr(settings.core, "concurrency", concurrency_limit)
|
|
13
|
+
|
|
14
|
+
active = 0
|
|
15
|
+
max_active = 0
|
|
16
|
+
|
|
17
|
+
async def task(number: int):
|
|
18
|
+
nonlocal active, max_active
|
|
19
|
+
active += 1
|
|
20
|
+
max_active = max(max_active, active)
|
|
21
|
+
await asyncio.sleep(0.05)
|
|
22
|
+
active -= 1
|
|
23
|
+
return number * 2
|
|
24
|
+
|
|
25
|
+
results = await bounded_gather(task(index) for index in range(10))
|
|
26
|
+
|
|
27
|
+
assert max_active <= concurrency_limit
|
|
28
|
+
assert results == tuple(index * 2 for index in range(10))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_bounded_gather_returns_exceptions(monkeypatch: pytest.MonkeyPatch):
|
|
33
|
+
monkeypatch.setattr(settings.core, "concurrency", 2)
|
|
34
|
+
|
|
35
|
+
async def ok_task():
|
|
36
|
+
await asyncio.sleep(0.01)
|
|
37
|
+
return "ok"
|
|
38
|
+
|
|
39
|
+
async def fail_task():
|
|
40
|
+
raise ValueError("boom")
|
|
41
|
+
|
|
42
|
+
results = await bounded_gather([ok_task(), fail_task(), ok_task()])
|
|
43
|
+
|
|
44
|
+
assert len(results) == 3
|
|
45
|
+
assert any(isinstance(result, Exception) for result in results)
|
|
46
|
+
assert any(r == "ok" for r in results)
|
|
@@ -4,7 +4,7 @@ from ai_review import config
|
|
|
4
4
|
from ai_review.libs.config.review import ReviewMode
|
|
5
5
|
from ai_review.libs.diff.models import Diff, DiffFile, FileMode
|
|
6
6
|
from ai_review.services.diff.service import DiffService
|
|
7
|
-
from ai_review.tests.fixtures.git import FakeGitService
|
|
7
|
+
from ai_review.tests.fixtures.services.git import FakeGitService
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@pytest.fixture
|
|
@@ -5,7 +5,7 @@ import pytest
|
|
|
5
5
|
|
|
6
6
|
from ai_review.libs.diff.models import Diff, DiffFile, DiffHunk, DiffRange, DiffLineType, FileMode
|
|
7
7
|
from ai_review.services.diff import tools
|
|
8
|
-
from ai_review.tests.fixtures.git import FakeGitService
|
|
8
|
+
from ai_review.tests.fixtures.services.git import FakeGitService
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# ---------- normalize_file_path ----------
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.llm.claude.client import ClaudeLLMClient
|
|
4
|
+
from ai_review.services.llm.factory import get_llm_client
|
|
5
|
+
from ai_review.services.llm.gemini.client import GeminiLLMClient
|
|
6
|
+
from ai_review.services.llm.openai.client import OpenAILLMClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.usefixtures("openai_http_client_config")
|
|
10
|
+
def test_get_llm_client_returns_openai(monkeypatch: pytest.MonkeyPatch):
|
|
11
|
+
client = get_llm_client()
|
|
12
|
+
assert isinstance(client, OpenAILLMClient)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.usefixtures("gemini_http_client_config")
|
|
16
|
+
def test_get_llm_client_returns_gemini(monkeypatch: pytest.MonkeyPatch):
|
|
17
|
+
client = get_llm_client()
|
|
18
|
+
assert isinstance(client, GeminiLLMClient)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.usefixtures("claude_http_client_config")
|
|
22
|
+
def test_get_llm_client_returns_claude(monkeypatch: pytest.MonkeyPatch):
|
|
23
|
+
client = get_llm_client()
|
|
24
|
+
assert isinstance(client, ClaudeLLMClient)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_get_llm_client_unsupported_provider(monkeypatch: pytest.MonkeyPatch):
|
|
28
|
+
monkeypatch.setattr("ai_review.services.llm.factory.settings.llm.provider", "DEEPSEEK")
|
|
29
|
+
with pytest.raises(ValueError):
|
|
30
|
+
get_llm_client()
|