Skip to content

Commit d7c32ff

Browse files
authored
Initial version of find_closing_pull_requests
1 parent ff6e859 commit d7c32ff

File tree

5 files changed

+662
-0
lines changed

5 files changed

+662
-0
lines changed

e2e/e2e_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
gogithub "github.com/google/go-github/v73/github"
2222
mcpClient "github.com/mark3labs/mcp-go/client"
2323
"github.com/mark3labs/mcp-go/mcp"
24+
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526
)
2627

@@ -1624,3 +1625,155 @@ func TestPullRequestReviewDeletion(t *testing.T) {
16241625
require.NoError(t, err, "expected to unmarshal text content successfully")
16251626
require.Len(t, noReviews, 0, "expected to find no reviews")
16261627
}
1628+
1629+
func TestFindClosingPullRequests(t *testing.T) {
1630+
t.Parallel()
1631+
1632+
mcpClient := setupMCPClient(t, withToolsets([]string{"issues"}))
1633+
1634+
ctx := context.Background()
1635+
1636+
// Test with well-known GitHub repositories and issues
1637+
testCases := []struct {
1638+
name string
1639+
issues []interface{}
1640+
limit int
1641+
expectError bool
1642+
expectedResults int
1643+
expectSomeWithClosingPR bool
1644+
}{
1645+
{
1646+
name: "Single issue test - should handle gracefully even if no closing PRs",
1647+
issues: []interface{}{"octocat/Hello-World#1"},
1648+
limit: 5,
1649+
expectError: false,
1650+
expectedResults: 1,
1651+
},
1652+
{
1653+
name: "Multiple issues test",
1654+
issues: []interface{}{"octocat/Hello-World#1", "github/docs#1"},
1655+
limit: 3,
1656+
expectError: false,
1657+
expectedResults: 2,
1658+
},
1659+
{
1660+
name: "Invalid issue format should return error",
1661+
issues: []interface{}{"invalid-format"},
1662+
expectError: true,
1663+
},
1664+
{
1665+
name: "Empty issues array should return error",
1666+
issues: []interface{}{},
1667+
expectError: true,
1668+
},
1669+
{
1670+
name: "Limit too high should return error",
1671+
issues: []interface{}{"octocat/Hello-World#1"},
1672+
limit: 150,
1673+
expectError: true,
1674+
},
1675+
}
1676+
1677+
for _, tc := range testCases {
1678+
t.Run(tc.name, func(t *testing.T) {
1679+
// Prepare the request
1680+
findClosingPRsRequest := mcp.CallToolRequest{}
1681+
findClosingPRsRequest.Params.Name = "find_closing_pull_requests"
1682+
1683+
// Build arguments map
1684+
args := map[string]any{
1685+
"issues": tc.issues,
1686+
}
1687+
if tc.limit > 0 {
1688+
args["limit"] = tc.limit
1689+
}
1690+
findClosingPRsRequest.Params.Arguments = args
1691+
1692+
t.Logf("Calling find_closing_pull_requests with issues: %v", tc.issues)
1693+
resp, err := mcpClient.CallTool(ctx, findClosingPRsRequest)
1694+
1695+
if tc.expectError {
1696+
// We expect either an error or an error response
1697+
if err != nil {
1698+
t.Logf("Expected error occurred: %v", err)
1699+
return
1700+
}
1701+
require.True(t, resp.IsError, "Expected error response")
1702+
t.Logf("Expected error in response: %+v", resp)
1703+
return
1704+
}
1705+
1706+
require.NoError(t, err, "expected to call 'find_closing_pull_requests' tool successfully")
1707+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1708+
1709+
// Verify we got content
1710+
require.NotEmpty(t, resp.Content, "Expected response content")
1711+
1712+
textContent, ok := resp.Content[0].(mcp.TextContent)
1713+
require.True(t, ok, "expected content to be of type TextContent")
1714+
1715+
t.Logf("Response: %s", textContent.Text)
1716+
1717+
// Parse the JSON response
1718+
var response struct {
1719+
Results []struct {
1720+
Issue string `json:"issue"`
1721+
Owner string `json:"owner"`
1722+
Repo string `json:"repo"`
1723+
IssueNumber int `json:"issueNumber"`
1724+
ClosingPullRequests []struct {
1725+
Number int `json:"number"`
1726+
Title string `json:"title"`
1727+
Body string `json:"body"`
1728+
State string `json:"state"`
1729+
URL string `json:"url"`
1730+
Merged bool `json:"merged"`
1731+
} `json:"closingPullRequests"`
1732+
TotalCount int `json:"totalCount"`
1733+
Error string `json:"error,omitempty"`
1734+
} `json:"results"`
1735+
}
1736+
1737+
err = json.Unmarshal([]byte(textContent.Text), &response)
1738+
require.NoError(t, err, "expected to unmarshal response successfully")
1739+
1740+
// Verify the response structure
1741+
require.Len(t, response.Results, tc.expectedResults, "Expected specific number of results")
1742+
1743+
// Log and verify each result
1744+
for i, result := range response.Results {
1745+
t.Logf("Result %d:", i+1)
1746+
t.Logf(" Issue: %s", result.Issue)
1747+
t.Logf(" Owner: %s, Repo: %s, Number: %d", result.Owner, result.Repo, result.IssueNumber)
1748+
t.Logf(" Total closing PRs: %d", result.TotalCount)
1749+
if result.Error != "" {
1750+
t.Logf(" Error: %s", result.Error)
1751+
}
1752+
1753+
// Verify basic structure
1754+
assert.NotEmpty(t, result.Issue, "Issue reference should not be empty")
1755+
assert.NotEmpty(t, result.Owner, "Owner should not be empty")
1756+
assert.NotEmpty(t, result.Repo, "Repo should not be empty")
1757+
assert.Greater(t, result.IssueNumber, 0, "Issue number should be positive")
1758+
1759+
// Log details of any closing PRs found
1760+
for j, pr := range result.ClosingPullRequests {
1761+
t.Logf(" Closing PR %d:", j+1)
1762+
t.Logf(" Number: %d", pr.Number)
1763+
t.Logf(" Title: %s", pr.Title)
1764+
t.Logf(" State: %s, Merged: %t", pr.State, pr.Merged)
1765+
t.Logf(" URL: %s", pr.URL)
1766+
1767+
// Verify PR structure
1768+
assert.Greater(t, pr.Number, 0, "PR number should be positive")
1769+
assert.NotEmpty(t, pr.Title, "PR title should not be empty")
1770+
assert.NotEmpty(t, pr.State, "PR state should not be empty")
1771+
assert.NotEmpty(t, pr.URL, "PR URL should not be empty")
1772+
}
1773+
1774+
// The actual count of closing PRs should match the returned array length
1775+
assert.Equal(t, len(result.ClosingPullRequests), result.TotalCount, "ClosingPullRequests length should match TotalCount")
1776+
}
1777+
})
1778+
}
1779+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//go:build e2e
2+
3+
package github
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"os"
9+
"testing"
10+
11+
"github.com/github/github-mcp-server/pkg/translations"
12+
"github.com/google/go-github/v73/github"
13+
"github.com/shurcooL/githubv4"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// TestFindClosingPullRequestsIntegration tests the FindClosingPullRequests tool with real GitHub API calls
19+
func TestFindClosingPullRequestsIntegration(t *testing.T) {
20+
// This test requires a GitHub token
21+
token := os.Getenv("GITHUB_MCP_SERVER_E2E_TOKEN")
22+
if token == "" {
23+
t.Skip("GITHUB_MCP_SERVER_E2E_TOKEN environment variable is not set")
24+
}
25+
26+
// Create GitHub clients
27+
httpClient := github.NewClient(nil).WithAuthToken(token).Client()
28+
gqlClient := githubv4.NewClient(httpClient)
29+
30+
getGQLClient := func(ctx context.Context) (*githubv4.Client, error) {
31+
return gqlClient, nil
32+
}
33+
34+
// Create the tool
35+
tool, handler := FindClosingPullRequests(getGQLClient, translations.NullTranslationHelper)
36+
37+
// Test cases with known GitHub issues that were closed by PRs
38+
testCases := []struct {
39+
name string
40+
issues []string
41+
expectedResults int
42+
expectSomeClosingPRs bool
43+
expectSpecificIssue string
44+
expectSpecificPRNumber int
45+
}{
46+
{
47+
name: "Single issue - VS Code well-known closed issue",
48+
issues: []string{"microsoft/vscode#123456"}, // This is a made-up issue for testing
49+
expectedResults: 1,
50+
expectSomeClosingPRs: false, // We expect this to not exist or have no closing PRs
51+
},
52+
{
53+
name: "Multiple issues with mixed results",
54+
issues: []string{"octocat/Hello-World#1", "microsoft/vscode#999999"},
55+
expectedResults: 2,
56+
expectSomeClosingPRs: false, // These are likely non-existent or have no closing PRs
57+
},
58+
{
59+
name: "Issue from a popular repo - React",
60+
issues: []string{"facebook/react#1"}, // Very first issue in React repo
61+
expectedResults: 1,
62+
},
63+
}
64+
65+
ctx := context.Background()
66+
67+
for _, tc := range testCases {
68+
t.Run(tc.name, func(t *testing.T) {
69+
// Create request arguments
70+
args := map[string]interface{}{
71+
"issues": tc.issues,
72+
"limit": 5,
73+
}
74+
75+
// Create mock request
76+
request := mockCallToolRequest{
77+
arguments: args,
78+
}
79+
80+
// Call the handler
81+
result, err := handler(ctx, request)
82+
83+
if err != nil {
84+
t.Logf("Error calling tool: %v", err)
85+
// For integration tests, we might expect some errors for non-existent issues
86+
// Let's check if it's a reasonable error
87+
assert.Contains(t, err.Error(), "failed to")
88+
return
89+
}
90+
91+
require.NotNil(t, result)
92+
assert.False(t, result.IsError, "Expected successful result")
93+
94+
// Parse the response
95+
textContent, ok := result.Content[0].(map[string]interface{})
96+
if !ok {
97+
// Try to get as text content
98+
if len(result.Content) > 0 {
99+
if textResult, ok := result.Content[0].(string); ok {
100+
t.Logf("Response: %s", textResult)
101+
102+
// Parse JSON response
103+
var response struct {
104+
Results []FindClosingPRsResult `json:"results"`
105+
}
106+
err := json.Unmarshal([]byte(textResult), &response)
107+
require.NoError(t, err, "Failed to parse JSON response")
108+
109+
// Verify structure
110+
assert.Len(t, response.Results, tc.expectedResults, "Expected specific number of results")
111+
112+
for i, result := range response.Results {
113+
t.Logf("Issue %d: %s", i+1, result.Issue)
114+
t.Logf(" Owner: %s, Repo: %s, Number: %d", result.Owner, result.Repo, result.IssueNumber)
115+
t.Logf(" Total closing PRs: %d", result.TotalCount)
116+
t.Logf(" Error: %s", result.Error)
117+
118+
// Verify basic structure
119+
assert.NotEmpty(t, result.Issue, "Issue reference should not be empty")
120+
assert.NotEmpty(t, result.Owner, "Owner should not be empty")
121+
assert.NotEmpty(t, result.Repo, "Repo should not be empty")
122+
assert.Greater(t, result.IssueNumber, 0, "Issue number should be positive")
123+
124+
// Log closing PRs if any
125+
for j, pr := range result.ClosingPullRequests {
126+
t.Logf(" PR %d: #%d - %s", j+1, pr.Number, pr.Title)
127+
t.Logf(" State: %s, Merged: %t", pr.State, pr.Merged)
128+
t.Logf(" URL: %s", pr.URL)
129+
}
130+
131+
// Check for expected specific results
132+
if tc.expectSpecificIssue != "" && result.Issue == tc.expectSpecificIssue {
133+
if tc.expectSpecificPRNumber > 0 {
134+
found := false
135+
for _, pr := range result.ClosingPullRequests {
136+
if pr.Number == tc.expectSpecificPRNumber {
137+
found = true
138+
break
139+
}
140+
}
141+
assert.True(t, found, "Expected to find specific PR number")
142+
}
143+
}
144+
}
145+
146+
return
147+
}
148+
}
149+
t.Fatalf("Unexpected content type: %T", result.Content[0])
150+
}
151+
152+
t.Logf("Response content: %+v", textContent)
153+
})
154+
}
155+
}
156+
157+
// mockCallToolRequest implements the mcp.CallToolRequest interface for testing
158+
type mockCallToolRequest struct {
159+
arguments map[string]interface{}
160+
}
161+
162+
func (m mockCallToolRequest) GetArguments() map[string]interface{} {
163+
return m.arguments
164+
}

0 commit comments

Comments
 (0)