arcade-mcp/toolkits/github/arcade_github/tests/test_pull_requests.py
Eric Gustin 7e352fbe91
Add Github Toolkit (#75)
### 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>
2024-10-02 10:40:17 -07:00

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,
)