xai-review 0.21.0__py3-none-any.whl → 0.22.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 (60) hide show
  1. ai_review/clients/claude/client.py +1 -1
  2. ai_review/clients/gemini/client.py +1 -1
  3. ai_review/clients/github/client.py +1 -1
  4. ai_review/clients/github/pr/client.py +64 -16
  5. ai_review/clients/github/pr/schema/comments.py +4 -0
  6. ai_review/clients/github/pr/schema/files.py +4 -0
  7. ai_review/clients/github/pr/schema/reviews.py +4 -0
  8. ai_review/clients/github/pr/types.py +49 -0
  9. ai_review/clients/gitlab/client.py +1 -1
  10. ai_review/clients/gitlab/mr/client.py +25 -8
  11. ai_review/clients/gitlab/mr/schema/discussions.py +4 -0
  12. ai_review/clients/gitlab/mr/schema/notes.py +4 -0
  13. ai_review/clients/gitlab/mr/types.py +35 -0
  14. ai_review/clients/openai/client.py +1 -1
  15. ai_review/config.py +2 -0
  16. ai_review/libs/asynchronous/gather.py +6 -3
  17. ai_review/libs/config/core.py +5 -0
  18. ai_review/libs/http/event_hooks/logger.py +5 -2
  19. ai_review/libs/http/transports/retry.py +23 -6
  20. ai_review/tests/fixtures/clients/claude.py +22 -0
  21. ai_review/tests/fixtures/clients/gemini.py +21 -0
  22. ai_review/tests/fixtures/clients/github.py +181 -0
  23. ai_review/tests/fixtures/clients/gitlab.py +150 -0
  24. ai_review/tests/fixtures/clients/openai.py +21 -0
  25. ai_review/tests/fixtures/services/__init__.py +0 -0
  26. ai_review/tests/fixtures/services/review/__init__.py +0 -0
  27. ai_review/tests/suites/clients/claude/test_client.py +1 -20
  28. ai_review/tests/suites/clients/gemini/test_client.py +1 -19
  29. ai_review/tests/suites/clients/github/test_client.py +1 -23
  30. ai_review/tests/suites/clients/gitlab/test_client.py +1 -22
  31. ai_review/tests/suites/clients/openai/test_client.py +1 -19
  32. ai_review/tests/suites/libs/asynchronous/__init__.py +0 -0
  33. ai_review/tests/suites/libs/asynchronous/test_gather.py +46 -0
  34. ai_review/tests/suites/services/diff/test_service.py +1 -1
  35. ai_review/tests/suites/services/diff/test_tools.py +1 -1
  36. ai_review/tests/suites/services/llm/__init__.py +0 -0
  37. ai_review/tests/suites/services/llm/test_factory.py +30 -0
  38. ai_review/tests/suites/services/review/test_service.py +9 -9
  39. ai_review/tests/suites/services/vcs/__init__.py +0 -0
  40. ai_review/tests/suites/services/vcs/github/__init__.py +0 -0
  41. ai_review/tests/suites/services/vcs/github/test_service.py +114 -0
  42. ai_review/tests/suites/services/vcs/gitlab/__init__.py +0 -0
  43. ai_review/tests/suites/services/vcs/gitlab/test_service.py +123 -0
  44. ai_review/tests/suites/services/vcs/test_factory.py +23 -0
  45. {xai_review-0.21.0.dist-info → xai_review-0.22.0.dist-info}/METADATA +2 -2
  46. {xai_review-0.21.0.dist-info → xai_review-0.22.0.dist-info}/RECORD +60 -40
  47. /ai_review/tests/fixtures/{review → clients}/__init__.py +0 -0
  48. /ai_review/tests/fixtures/{artifacts.py → services/artifacts.py} +0 -0
  49. /ai_review/tests/fixtures/{cost.py → services/cost.py} +0 -0
  50. /ai_review/tests/fixtures/{diff.py → services/diff.py} +0 -0
  51. /ai_review/tests/fixtures/{git.py → services/git.py} +0 -0
  52. /ai_review/tests/fixtures/{llm.py → services/llm.py} +0 -0
  53. /ai_review/tests/fixtures/{prompt.py → services/prompt.py} +0 -0
  54. /ai_review/tests/fixtures/{review → services/review}/inline.py +0 -0
  55. /ai_review/tests/fixtures/{review → services/review}/summary.py +0 -0
  56. /ai_review/tests/fixtures/{vcs.py → services/vcs.py} +0 -0
  57. {xai_review-0.21.0.dist-info → xai_review-0.22.0.dist-info}/WHEEL +0 -0
  58. {xai_review-0.21.0.dist-info → xai_review-0.22.0.dist-info}/entry_points.txt +0 -0
  59. {xai_review-0.21.0.dist-info → xai_review-0.22.0.dist-info}/licenses/LICENSE +0 -0
  60. {xai_review-0.21.0.dist-info → xai_review-0.22.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()
@@ -1,15 +1,15 @@
1
1
  import pytest
2
2
 
3
3
  from ai_review.services.review.service import ReviewService
4
- from ai_review.tests.fixtures.artifacts import FakeArtifactsService
5
- from ai_review.tests.fixtures.cost import FakeCostService
6
- from ai_review.tests.fixtures.diff import FakeDiffService
7
- from ai_review.tests.fixtures.git import FakeGitService
8
- from ai_review.tests.fixtures.llm import FakeLLMClient
9
- from ai_review.tests.fixtures.prompt import FakePromptService
10
- from ai_review.tests.fixtures.review.inline import FakeInlineCommentService
11
- from ai_review.tests.fixtures.review.summary import FakeSummaryCommentService
12
- from ai_review.tests.fixtures.vcs import FakeVCSClient
4
+ from ai_review.tests.fixtures.services.artifacts import FakeArtifactsService
5
+ from ai_review.tests.fixtures.services.cost import FakeCostService
6
+ from ai_review.tests.fixtures.services.diff import FakeDiffService
7
+ from ai_review.tests.fixtures.services.git import FakeGitService
8
+ from ai_review.tests.fixtures.services.llm import FakeLLMClient
9
+ from ai_review.tests.fixtures.services.prompt import FakePromptService
10
+ from ai_review.tests.fixtures.services.review.inline import FakeInlineCommentService
11
+ from ai_review.tests.fixtures.services.review.summary import FakeSummaryCommentService
12
+ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
13
13
 
14
14
 
15
15
  @pytest.fixture
File without changes
File without changes