Skip to content

Commit b9a06d0

Browse files
williammartinmartinajir
authored andcommitted
Support assigning copilot to issues
Co-authored-by: Martina Jireckova <[email protected]>
1 parent 4ccedee commit b9a06d0

File tree

6 files changed

+755
-9
lines changed

6 files changed

+755
-9
lines changed

e2e/e2e_test.go

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/base64"
88
"encoding/json"
99
"fmt"
10+
"net/http"
1011
"os"
1112
"os/exec"
1213
"slices"
@@ -210,7 +211,6 @@ func TestGetMe(t *testing.T) {
210211
t.Parallel()
211212

212213
mcpClient := setupMCPClient(t)
213-
214214
ctx := context.Background()
215215

216216
// When we call the "get_me" tool
@@ -795,14 +795,13 @@ func TestDirectoryDeletion(t *testing.T) {
795795
}
796796

797797
func TestRequestCopilotReview(t *testing.T) {
798+
t.Parallel()
799+
798800
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
799801
t.Skip("Skipping test because the host does not support copilot reviews")
800802
}
801803

802-
t.Parallel()
803-
804804
mcpClient := setupMCPClient(t)
805-
806805
ctx := context.Background()
807806

808807
// First, who am I
@@ -943,6 +942,112 @@ func TestRequestCopilotReview(t *testing.T) {
943942
require.Equal(t, "Bot", *reviewRequests.Users[0].Type, "expected review request to be for Bot")
944943
}
945944

945+
func TestAssignCopilotToIssue(t *testing.T) {
946+
t.Parallel()
947+
948+
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
949+
t.Skip("Skipping test because the host does not support copilot being assigned to issues")
950+
}
951+
952+
mcpClient := setupMCPClient(t)
953+
ctx := context.Background()
954+
955+
// First, who am I
956+
getMeRequest := mcp.CallToolRequest{}
957+
getMeRequest.Params.Name = "get_me"
958+
959+
t.Log("Getting current user...")
960+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
961+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
962+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
963+
964+
require.False(t, resp.IsError, "expected result not to be an error")
965+
require.Len(t, resp.Content, 1, "expected content to have one item")
966+
967+
textContent, ok := resp.Content[0].(mcp.TextContent)
968+
require.True(t, ok, "expected content to be of type TextContent")
969+
970+
var trimmedGetMeText struct {
971+
Login string `json:"login"`
972+
}
973+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
974+
require.NoError(t, err, "expected to unmarshal text content successfully")
975+
976+
currentOwner := trimmedGetMeText.Login
977+
978+
// Then create a repository with a README (via autoInit)
979+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
980+
createRepoRequest := mcp.CallToolRequest{}
981+
createRepoRequest.Params.Name = "create_repository"
982+
createRepoRequest.Params.Arguments = map[string]any{
983+
"name": repoName,
984+
"private": true,
985+
"autoInit": true,
986+
}
987+
988+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
989+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
990+
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
991+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
992+
993+
// Cleanup the repository after the test
994+
t.Cleanup(func() {
995+
// MCP Server doesn't support deletions, but we can use the GitHub Client
996+
ghClient := getRESTClient(t)
997+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
998+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
999+
require.NoError(t, err, "expected to delete repository successfully")
1000+
})
1001+
1002+
// Create an issue
1003+
createIssueRequest := mcp.CallToolRequest{}
1004+
createIssueRequest.Params.Name = "create_issue"
1005+
createIssueRequest.Params.Arguments = map[string]any{
1006+
"owner": currentOwner,
1007+
"repo": repoName,
1008+
"title": "Test issue to assign copilot to",
1009+
}
1010+
1011+
t.Logf("Creating issue in %s/%s...", currentOwner, repoName)
1012+
resp, err = mcpClient.CallTool(ctx, createIssueRequest)
1013+
require.NoError(t, err, "expected to call 'create_issue' tool successfully")
1014+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1015+
1016+
// Assign copilot to the issue
1017+
assignCopilotRequest := mcp.CallToolRequest{}
1018+
assignCopilotRequest.Params.Name = "assign_copilot_to_issue"
1019+
assignCopilotRequest.Params.Arguments = map[string]any{
1020+
"owner": currentOwner,
1021+
"repo": repoName,
1022+
"issueNumber": 1,
1023+
}
1024+
1025+
t.Logf("Assigning copilot to issue in %s/%s...", currentOwner, repoName)
1026+
resp, err = mcpClient.CallTool(ctx, assignCopilotRequest)
1027+
require.NoError(t, err, "expected to call 'assign_copilot_to_issue' tool successfully")
1028+
1029+
textContent, ok = resp.Content[0].(mcp.TextContent)
1030+
require.True(t, ok, "expected content to be of type TextContent")
1031+
1032+
possibleExpectedFailure := "copilot isn't available as an assignee for this issue. Please inform the user to visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot for more information."
1033+
if resp.IsError && textContent.Text == possibleExpectedFailure {
1034+
t.Skip("skipping because copilot wasn't available as an assignee on this issue, it's likely that the owner doesn't have copilot enabled in their settings")
1035+
}
1036+
1037+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1038+
1039+
require.Equal(t, "successfully assigned copilot to issue", textContent.Text)
1040+
1041+
// Check that copilot is assigned to the issue
1042+
// MCP Server doesn't support getting assignees yet
1043+
ghClient := getRESTClient(t)
1044+
assignees, response, err := ghClient.Issues.Get(context.Background(), currentOwner, repoName, 1)
1045+
require.NoError(t, err, "expected to get issue successfully")
1046+
require.Equal(t, http.StatusOK, response.StatusCode, "expected to get issue successfully")
1047+
require.Len(t, assignees.Assignees, 1, "expected to find one assignee")
1048+
require.Equal(t, "Copilot", *assignees.Assignees[0].Login, "expected copilot to be assigned to the issue")
1049+
}
1050+
9461051
func TestPullRequestAtomicCreateAndSubmit(t *testing.T) {
9471052
t.Parallel()
9481053

@@ -1145,7 +1250,7 @@ func TestPullRequestReviewCommentSubmit(t *testing.T) {
11451250

11461251
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
11471252
_, err = mcpClient.CallTool(ctx, createRepoRequest)
1148-
require.NoError(t, err, "expected to call 'get_me' tool successfully")
1253+
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
11491254
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
11501255

11511256
// Cleanup the repository after the test

internal/githubv4mock/objects_are_equal_values.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions.go#L166
22
// because I do not want to take a dependency on the entire testify module just to use this equality check.
33
//
4+
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
5+
//
46
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
57
//
68
// MIT License
@@ -69,8 +71,10 @@ func objectsAreEqualValues(expected, actual any) bool {
6971
//
7072
// This function does no assertion of any kind.
7173
func objectsAreEqual(expected, actual any) bool {
72-
if expected == nil || actual == nil {
73-
return expected == actual
74+
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
75+
// This is required because when a nil is provided as a variable, the type is not known.
76+
if isNil(expected) && isNil(actual) {
77+
return true
7478
}
7579

7680
exp, ok := expected.([]byte)
@@ -94,3 +98,16 @@ func objectsAreEqual(expected, actual any) bool {
9498
func isNumericType(t reflect.Type) bool {
9599
return t.Kind() >= reflect.Int && t.Kind() <= reflect.Complex128
96100
}
101+
102+
func isNil(i any) bool {
103+
if i == nil {
104+
return true
105+
}
106+
v := reflect.ValueOf(i)
107+
switch v.Kind() {
108+
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
109+
return v.IsNil()
110+
default:
111+
return false
112+
}
113+
}

internal/githubv4mock/objects_are_equal_values_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions_test.go#L140-L174
22
//
3+
// There is a modification to test objectsAreEqualValues to check that typed nils are equal, even if their types are different.
4+
35
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
46
//
57
// MIT License
@@ -55,6 +57,8 @@ func TestObjectsAreEqualValues(t *testing.T) {
5557
{3.14, complex128(1e+100 + 1e+100i), false},
5658
{complex128(1e+10 + 1e+10i), complex64(1e+10 + 1e+10i), true},
5759
{complex64(1e+10 + 1e+10i), complex128(1e+10 + 1e+10i), true},
60+
{(*string)(nil), nil, true}, // typed nil vs untyped nil
61+
{(*string)(nil), (*int)(nil), true}, // different typed nils
5862
}
5963

6064
for _, c := range cases {

0 commit comments

Comments
 (0)