### Adds the following tools to the Github Toolkit:
1. CreateIssueComment
2. SetStarred
3. CountStargazers
4. ListOrgRepositories
5. GetRepository
6. ListRepositoryActivities
7. ListReviewCommentsInARepository
8. ListPullRequests
9. GetPullRequest
10. UpdatePullRequest
11. ListPullRequestCommits
12. CreateReplyForReviewComment
13. ListReviewCommentsOnPullRequest
14. CreateReviewComment
Adds evals for all of these tools and unit tests.
---------
Co-authored-by: Sam Partee <sam@arcade-ai.com>
359 lines
11 KiB
Python
359 lines
11 KiB
Python
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from arcade_github.tools.models import (
|
|
DiffSide,
|
|
ReviewCommentSubjectType,
|
|
)
|
|
from arcade_github.tools.pull_requests import (
|
|
create_reply_for_review_comment,
|
|
create_review_comment,
|
|
get_pull_request,
|
|
list_pull_request_commits,
|
|
list_pull_requests,
|
|
list_review_comments_on_pull_request,
|
|
update_pull_request,
|
|
)
|
|
from httpx import Response
|
|
|
|
from arcade.core.errors import RetryableToolError, ToolExecutionError
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_context():
|
|
context = AsyncMock()
|
|
context.authorization.token = "mock_token" # noqa: S105
|
|
return context
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_client():
|
|
with patch("arcade_github.tools.pull_requests.httpx.AsyncClient") as client:
|
|
yield client.return_value.__aenter__.return_value
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"func,args,status_code,json_response,expected_result,error_message",
|
|
[
|
|
(list_pull_requests, ("owner", "repo"), 200, [], '{"pull_requests": []}', None),
|
|
(
|
|
get_pull_request,
|
|
("owner", "repo", 1),
|
|
404,
|
|
{"message": "Not Found"},
|
|
None,
|
|
"Error accessing.*: Resource not found",
|
|
),
|
|
(
|
|
update_pull_request,
|
|
("owner", "repo", 1, "New Title"),
|
|
409,
|
|
{"message": "Conflict"},
|
|
None,
|
|
"Error accessing.*: Failed to process request",
|
|
),
|
|
(
|
|
list_pull_request_commits,
|
|
("owner", "repo", 1),
|
|
500,
|
|
{"message": "Internal Server Error"},
|
|
None,
|
|
"Error accessing.*: Failed to process request",
|
|
),
|
|
(
|
|
list_review_comments_on_pull_request,
|
|
("owner", "repo", 1),
|
|
403,
|
|
{"message": "API rate limit exceeded"},
|
|
None,
|
|
"Error accessing.*: Forbidden",
|
|
),
|
|
],
|
|
)
|
|
async def test_pull_request_functions(
|
|
mock_context,
|
|
mock_client,
|
|
func,
|
|
args,
|
|
status_code,
|
|
json_response,
|
|
expected_result,
|
|
error_message,
|
|
):
|
|
mock_client.get.return_value = mock_client.post.return_value = (
|
|
mock_client.patch.return_value
|
|
) = Response(status_code, json=json_response)
|
|
|
|
if error_message:
|
|
with pytest.raises(ToolExecutionError, match=error_message):
|
|
await func(mock_context, *args)
|
|
else:
|
|
result = await func(mock_context, *args)
|
|
assert result == expected_result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"func,args,json_response,expected_assertions",
|
|
[
|
|
(
|
|
list_pull_requests,
|
|
("owner", "repo"),
|
|
[
|
|
{
|
|
"number": 1,
|
|
"title": "Test PR",
|
|
"body": "This is a test PR",
|
|
"state": "open",
|
|
"html_url": "https://github.com/owner/repo/pull/1",
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-01T12:00:00Z",
|
|
"user": {"login": "testuser"},
|
|
"base": {"ref": "main"},
|
|
"head": {"ref": "feature-branch"},
|
|
}
|
|
],
|
|
["Test PR", "https://github.com/owner/repo/pull/1"],
|
|
),
|
|
(
|
|
update_pull_request,
|
|
("owner", "repo", 1, "Updated PR Title", "Updated PR body"),
|
|
{
|
|
"number": 1,
|
|
"title": "Updated PR Title",
|
|
"body": "Updated PR body",
|
|
"state": "open",
|
|
"html_url": "https://github.com/owner/repo/pull/1",
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-02T12:00:00Z",
|
|
"user": {"login": "testuser"},
|
|
},
|
|
["Updated PR Title", "Updated PR body"],
|
|
),
|
|
(
|
|
list_pull_request_commits,
|
|
("owner", "repo", 1),
|
|
[
|
|
{
|
|
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
"commit": {
|
|
"author": {
|
|
"name": "Test Author",
|
|
"email": "author@example.com",
|
|
"date": "2023-05-01T12:00:00Z",
|
|
},
|
|
"message": "Test commit message",
|
|
},
|
|
}
|
|
],
|
|
["6dcb09b5b57875f334f61aebed695e2e4193db5e", "Test commit message"],
|
|
),
|
|
(
|
|
create_reply_for_review_comment,
|
|
("owner", "repo", 1, 42, "Thanks for the suggestion."),
|
|
{
|
|
"id": 123,
|
|
"body": "Thanks for the suggestion.",
|
|
"user": {"login": "testuser"},
|
|
"created_at": "2023-05-02T12:00:00Z",
|
|
"updated_at": "2023-05-02T12:00:00Z",
|
|
},
|
|
["Thanks for the suggestion.", "testuser"],
|
|
),
|
|
(
|
|
list_review_comments_on_pull_request,
|
|
("owner", "repo", 1),
|
|
[
|
|
{
|
|
"id": 1,
|
|
"body": "Great changes!",
|
|
"user": {"login": "reviewer1"},
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-01T12:00:00Z",
|
|
"path": "file1.txt",
|
|
"line": 5,
|
|
}
|
|
],
|
|
["Great changes!", "reviewer1", "file1.txt"],
|
|
),
|
|
(
|
|
get_pull_request,
|
|
("owner", "repo", 1, False, False),
|
|
{
|
|
"number": 1,
|
|
"title": "Test PR",
|
|
"body": "This is a test PR",
|
|
"state": "open",
|
|
"html_url": "https://github.com/owner/repo/pull/1",
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-01T12:00:00Z",
|
|
"user": {"login": "testuser"},
|
|
"base": {"ref": "main"},
|
|
"head": {"ref": "feature-branch"},
|
|
},
|
|
["Test PR", "https://github.com/owner/repo/pull/1"],
|
|
),
|
|
(
|
|
get_pull_request,
|
|
("owner", "repo", 1, True, False),
|
|
{
|
|
"number": 1,
|
|
"title": "Test PR",
|
|
"body": "This is a test PR",
|
|
"state": "open",
|
|
"html_url": "https://github.com/owner/repo/pull/1",
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-01T12:00:00Z",
|
|
"user": {"login": "testuser"},
|
|
"base": {"ref": "main"},
|
|
"head": {"ref": "feature-branch"},
|
|
"diff_content": "Sample diff content",
|
|
},
|
|
["Test PR", "https://github.com/owner/repo/pull/1", "diff_content"],
|
|
),
|
|
(
|
|
create_review_comment,
|
|
(
|
|
"owner",
|
|
"repo",
|
|
1,
|
|
"Great changes!",
|
|
"file1.txt",
|
|
"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
1,
|
|
2,
|
|
DiffSide.RIGHT,
|
|
None,
|
|
ReviewCommentSubjectType.LINE,
|
|
),
|
|
{
|
|
"id": 1,
|
|
"body": "Great changes!",
|
|
"path": "file1.txt",
|
|
"line": 2,
|
|
"side": "RIGHT",
|
|
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
"user": {"login": "testuser"},
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-01T12:00:00Z",
|
|
"html_url": "https://github.com/owner/repo/pull/1#discussion_r1",
|
|
},
|
|
["Great changes!", "file1.txt", "6dcb09b5b57875f334f61aebed695e2e4193db5e"],
|
|
),
|
|
],
|
|
)
|
|
async def test_pull_request_functions_success(
|
|
mock_context, mock_client, func, args, json_response, expected_assertions
|
|
):
|
|
mock_client.get.return_value = mock_client.post.return_value = (
|
|
mock_client.patch.return_value
|
|
) = Response(200, json=json_response)
|
|
|
|
result = await func(mock_context, *args)
|
|
for assertion in expected_assertions:
|
|
assert assertion in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_review_comment_file_subject_type(mock_context, mock_client):
|
|
mock_client.post.return_value = Response(
|
|
200,
|
|
json={
|
|
"id": 1,
|
|
"body": "File comment",
|
|
"path": "file1.txt",
|
|
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
"user": {"login": "testuser"},
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-01T12:00:00Z",
|
|
"html_url": "https://github.com/owner/repo/pull/1#discussion_r1",
|
|
},
|
|
)
|
|
|
|
result = await create_review_comment(
|
|
mock_context,
|
|
"owner",
|
|
"repo",
|
|
1,
|
|
"File comment",
|
|
"file1.txt",
|
|
"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
subject_type=ReviewCommentSubjectType.FILE,
|
|
)
|
|
|
|
assert "File comment" in result
|
|
assert "file1.txt" in result
|
|
assert "6dcb09b5b57875f334f61aebed695e2e4193db5e" in result
|
|
assert "start_line" not in mock_client.post.call_args[1]["json"]
|
|
assert "end_line" not in mock_client.post.call_args[1]["json"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_review_comment_missing_commit_id(mock_context, mock_client):
|
|
mock_client.get.return_value = Response(
|
|
200,
|
|
json=[{"sha": "latest_commit_sha"}],
|
|
)
|
|
mock_client.post.return_value = Response(
|
|
200,
|
|
json={
|
|
"id": 1,
|
|
"body": "Comment with auto-fetched commit ID",
|
|
"path": "file1.txt",
|
|
"commit_id": "latest_commit_sha",
|
|
"user": {"login": "testuser"},
|
|
"created_at": "2023-05-01T12:00:00Z",
|
|
"updated_at": "2023-05-01T12:00:00Z",
|
|
"html_url": "https://github.com/owner/repo/pull/1#discussion_r1",
|
|
},
|
|
)
|
|
|
|
result = await create_review_comment(
|
|
mock_context,
|
|
"owner",
|
|
"repo",
|
|
1,
|
|
"Comment with auto-fetched commit ID",
|
|
"file1.txt",
|
|
start_line=1,
|
|
end_line=2,
|
|
)
|
|
|
|
assert "Comment with auto-fetched commit ID" in result
|
|
assert "latest_commit_sha" in result
|
|
assert mock_client.get.called
|
|
assert mock_client.post.called
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_review_comment_invalid_input(mock_context, mock_client):
|
|
with pytest.raises(
|
|
RetryableToolError, match="'start_line' and 'end_line' parameters are required"
|
|
):
|
|
await create_review_comment(
|
|
mock_context,
|
|
"owner",
|
|
"repo",
|
|
1,
|
|
"Invalid comment",
|
|
"file1.txt",
|
|
subject_type=ReviewCommentSubjectType.LINE,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_review_comment_no_commits(mock_context, mock_client):
|
|
mock_client.get.return_value = Response(200, json=[])
|
|
|
|
with pytest.raises(RetryableToolError, match="Failed to get the latest commit SHA"):
|
|
await create_review_comment(
|
|
mock_context,
|
|
"owner",
|
|
"repo",
|
|
1,
|
|
"Comment with no commits",
|
|
"file1.txt",
|
|
start_line=1,
|
|
end_line=2,
|
|
)
|