package github import ( "context" "encoding/json" "net/http" "strings" "sync/atomic" "testing" "time" "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v89/github" "github.com/google/jsonschema-go/jsonschema" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_GetPullRequest(t *testing.T) { // Verify tool definition once serverTool := PullRequestRead(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "method") assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "pullNumber") assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR for success case mockPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Head: &github.PullRequestBranch{ SHA: github.Ptr("abcd1234"), Ref: github.Ptr("feature-branch"), }, Base: &github.PullRequestBranch{ Ref: github.Ptr("main"), }, Body: github.Ptr("This is a test PR"), User: &github.User{ Login: github.Ptr("testuser"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedPR *github.PullRequest expectedErrMsg string lockdownEnabled bool restPermission string }{ { name: "successful PR fetch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPR), }), requestArgs: map[string]any{ "method": "get", "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedPR: mockPR, }, { name: "PR fetch fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }, }), requestArgs: map[string]any{ "method": "get", "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request", }, { name: "lockdown enabled - user lacks push access", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPR), }), requestArgs: map[string]any{ "method": "get", "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: true, expectedErrMsg: "access to pull request is restricted by lockdown mode", lockdownEnabled: true, restPermission: "read", }, { name: "lockdown enabled - private repository", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPR), }), requestArgs: map[string]any{ "method": "get", "owner": "owner2", "repo": "repo2", "pullNumber": float64(42), }, expectError: false, expectedPR: mockPR, lockdownEnabled: true, restPermission: "none", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := mustNewGHClient(t, tc.mockedClient) gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient()) var restClient *github.Client if tc.restPermission != "" { restClient = mockRESTPermissionServer(t, tc.restPermission, nil) } deps := BaseDeps{ Client: client, GQLClient: gqlClient, RepoAccessCache: stubRepoAccessCache(restClient, 5*time.Minute), Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled}), } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the minimal result var returnedPR MinimalPullRequest err = json.Unmarshal([]byte(textContent.Text), &returnedPR) require.NoError(t, err) assert.Equal(t, tc.expectedPR.GetNumber(), returnedPR.Number) assert.Equal(t, tc.expectedPR.GetTitle(), returnedPR.Title) assert.Equal(t, tc.expectedPR.GetState(), returnedPR.State) assert.Equal(t, tc.expectedPR.GetHTMLURL(), returnedPR.HTMLURL) }) } } func Test_UpdatePullRequest(t *testing.T) { // Verify tool definition once serverTool := UpdatePullRequest(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "update_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "pullNumber") assert.Contains(t, schema.Properties, "draft") assert.Contains(t, schema.Properties, "title") assert.Contains(t, schema.Properties, "body") assert.Contains(t, schema.Properties, "state") assert.Contains(t, schema.Properties, "base") assert.Contains(t, schema.Properties, "maintainer_can_modify") assert.Contains(t, schema.Properties, "reviewers") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR for success case mockUpdatedPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Updated Test PR Title"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Body: github.Ptr("Updated test PR body."), MaintainerCanModify: github.Ptr(false), Draft: github.Ptr(false), Base: &github.PullRequestBranch{ Ref: github.Ptr("develop"), }, } mockClosedPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("closed"), // State updated } // Mock PR for when there are no updates but we still need a response mockPRWithReviewers := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("open"), RequestedReviewers: []*github.User{ {Login: github.Ptr("reviewer1")}, {Login: github.Ptr("reviewer2")}, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedPR *github.PullRequest expectedErrMsg string }{ { name: "successful PR update (title, body, base, maintainer_can_modify)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{ "title": "Updated Test PR Title", "body": "Updated test PR body.", "base": "develop", "maintainer_can_modify": false, }).andThen( mockResponse(t, http.StatusOK, mockUpdatedPR), ), GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockUpdatedPR), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "title": "Updated Test PR Title", "body": "Updated test PR body.", "base": "develop", "maintainer_can_modify": false, }, expectError: false, expectedPR: mockUpdatedPR, }, { name: "successful PR update (state)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{ "state": "closed", }).andThen( mockResponse(t, http.StatusOK, mockClosedPR), ), GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockClosedPR), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "state": "closed", }, expectError: false, expectedPR: mockClosedPR, }, { name: "successful PR update with reviewers", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPRWithReviewers), GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPRWithReviewers), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "reviewers": []any{"reviewer1", "reviewer2"}, }, expectError: false, expectedPR: mockPRWithReviewers, }, { name: "successful PR update with user and team reviewers", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{ "reviewers": []any{"reviewer1"}, "team_reviewers": []any{"platform"}, }).andThen(mockResponse(t, http.StatusOK, mockPRWithReviewers)), GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPRWithReviewers), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "reviewers": []any{"reviewer1", "owner/platform"}, }, expectError: false, expectedPR: mockPRWithReviewers, }, { name: "successful PR update (title only)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{ "title": "Updated Test PR Title", }).andThen( mockResponse(t, http.StatusOK, mockUpdatedPR), ), GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockUpdatedPR), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "title": "Updated Test PR Title", }, expectError: false, expectedPR: mockUpdatedPR, }, { name: "no update parameters provided", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), // No API call expected requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), // No update fields }, expectError: false, // Error is returned in the result, not as Go error expectedErrMsg: "No update parameters provided", }, { name: "PR update fails (API error)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PatchReposPullsByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "title": "Invalid Title Causing Error", }, expectError: true, expectedErrMsg: "failed to update pull request", }, { name: "request reviewers fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Invalid reviewers"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "reviewers": []any{"invalid-user"}, }, expectError: true, expectedErrMsg: "failed to request reviewers", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := mustNewGHClient(t, tc.mockedClient) gqlClient := githubv4.NewClient(nil) deps := BaseDeps{ Client: client, GQLClient: gqlClient, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &request) // Verify results if tc.expectError || tc.expectedErrMsg != "" { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) if tc.expectedErrMsg != "" { assert.Contains(t, errorContent.Text, tc.expectedErrMsg) } return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content textContent := getTextResult(t, result) // Unmarshal and verify the minimal result var updateResp MinimalResponse err = json.Unmarshal([]byte(textContent.Text), &updateResp) require.NoError(t, err) assert.Equal(t, tc.expectedPR.GetHTMLURL(), updateResp.URL) }) } } func Test_UpdatePullRequest_Draft(t *testing.T) { // Setup mock PR for success case mockUpdatedPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR Title"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Body: github.Ptr("Test PR body."), MaintainerCanModify: github.Ptr(false), Draft: github.Ptr(false), // Updated to ready for review Base: &github.PullRequestBranch{ Ref: github.Ptr("main"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedPR *github.PullRequest expectedErrMsg string }{ { name: "successful draft update to ready for review", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse(map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": true, // Current state is draft }, }, }), ), githubv4mock.NewMutationMatcher( struct { MarkPullRequestReadyForReview struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } } `graphql:"markPullRequestReadyForReview(input: $input)"` }{}, githubv4.MarkPullRequestReadyForReviewInput{ PullRequestID: "PR_kwDOA0xdyM50BPaO", }, nil, githubv4mock.DataResponse(map[string]any{ "markPullRequestReadyForReview": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": false, }, }, }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "draft": false, }, expectError: false, expectedPR: mockUpdatedPR, }, { name: "successful convert pull request to draft", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse(map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": false, // Current state is draft }, }, }), ), githubv4mock.NewMutationMatcher( struct { ConvertPullRequestToDraft struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } } `graphql:"convertPullRequestToDraft(input: $input)"` }{}, githubv4.ConvertPullRequestToDraftInput{ PullRequestID: "PR_kwDOA0xdyM50BPaO", }, nil, githubv4mock.DataResponse(map[string]any{ "convertPullRequestToDraft": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": true, }, }, }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "draft": true, }, expectError: false, expectedPR: mockUpdatedPR, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // For draft-only tests, we need to mock both GraphQL and the final REST GET call restClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockUpdatedPR), })) gqlClient := githubv4.NewClient(tc.mockedClient) serverTool := UpdatePullRequest(translations.NullTranslationHelper) deps := BaseDeps{ Client: restClient, GQLClient: gqlClient, } handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) result, err := handler(ContextWithDeps(context.Background(), deps), &request) if tc.expectError || tc.expectedErrMsg != "" { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) if tc.expectedErrMsg != "" { assert.Contains(t, errorContent.Text, tc.expectedErrMsg) } return } require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) // Unmarshal and verify the minimal result var updateResp MinimalResponse err = json.Unmarshal([]byte(textContent.Text), &updateResp) require.NoError(t, err) assert.Equal(t, tc.expectedPR.GetHTMLURL(), updateResp.URL) }) } } func Test_ListPullRequests(t *testing.T) { // Verify tool definition once serverTool := ListPullRequests(translations.NullTranslationHelper) tool := serverTool.Tool // ListPullRequests is the FeatureFlagFieldsParam-enabled variant; it owns // the _ff_ snapshot. The canonical list_pull_requests.snap is owned by // LegacyListPullRequests (see Test_LegacyListPullRequests_Definition). require.NoError(t, toolsnaps.Test(tool.Name+"_ff_"+FeatureFlagFieldsParam, tool)) require.Equal(t, FeatureFlagFieldsParam, serverTool.FeatureFlagEnable) assert.Equal(t, "list_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "state") assert.Contains(t, schema.Properties, "head") assert.Contains(t, schema.Properties, "base") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "direction") assert.Contains(t, schema.Properties, "perPage") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "fields") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Setup mock PRs for success case mockPRs := []*github.PullRequest{ { Number: github.Ptr(42), Title: github.Ptr("First PR"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), }, { Number: github.Ptr(43), Title: github.Ptr("Second PR"), State: github.Ptr("closed"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/43"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedPRs []*github.PullRequest expectedErrMsg string }{ { name: "successful PRs listing", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepo: expectQueryParams(t, map[string]string{ "state": "all", "sort": "created", "direction": "desc", "per_page": "30", "page": "1", }).andThen( mockResponse(t, http.StatusOK, mockPRs), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "state": "all", "sort": "created", "direction": "desc", "perPage": float64(30), "page": float64(1), }, expectError: false, expectedPRs: mockPRs, }, { name: "PRs listing fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepo: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Invalid request"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "state": "open", }, expectError: true, expectedErrMsg: "failed to list pull requests", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := mustNewGHClient(t, tc.mockedClient) serverTool := ListPullRequests(translations.NullTranslationHelper) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedPRs []MinimalPullRequest err = json.Unmarshal([]byte(textContent.Text), &returnedPRs) require.NoError(t, err) assert.Len(t, returnedPRs, 2) assert.Equal(t, *tc.expectedPRs[0].Number, returnedPRs[0].Number) assert.Equal(t, *tc.expectedPRs[0].Title, returnedPRs[0].Title) assert.Equal(t, *tc.expectedPRs[0].State, returnedPRs[0].State) assert.Equal(t, *tc.expectedPRs[1].Number, returnedPRs[1].Number) assert.Equal(t, *tc.expectedPRs[1].Title, returnedPRs[1].Title) assert.Equal(t, *tc.expectedPRs[1].State, returnedPRs[1].State) }) } } func Test_MergePullRequest(t *testing.T) { // Verify tool definition once serverTool := MergePullRequest(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "merge_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "pullNumber") assert.Contains(t, schema.Properties, "commit_title") assert.Contains(t, schema.Properties, "commit_message") assert.Contains(t, schema.Properties, "merge_method") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock merge result for success case mockMergeResult := &github.PullRequestMergeResult{ Merged: github.Ptr(true), Message: github.Ptr("Pull Request successfully merged"), SHA: github.Ptr("abcd1234efgh5678"), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedMergeResult *github.PullRequestMergeResult expectedErrMsg string }{ { name: "successful merge", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PutReposPullsMergeByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{ "commit_title": "Merge PR #42", "commit_message": "Merging awesome feature", "merge_method": "squash", }).andThen( mockResponse(t, http.StatusOK, mockMergeResult), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "commit_title": "Merge PR #42", "commit_message": "Merging awesome feature", "merge_method": "squash", }, expectError: false, expectedMergeResult: mockMergeResult, }, { name: "merge fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PutReposPullsMergeByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusMethodNotAllowed) _, _ = w.Write([]byte(`{"message": "Pull request cannot be merged"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: true, expectedErrMsg: "failed to merge pull request", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := mustNewGHClient(t, tc.mockedClient) serverTool := MergePullRequest(translations.NullTranslationHelper) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult github.PullRequestMergeResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) assert.Equal(t, *tc.expectedMergeResult.Merged, *returnedResult.Merged) assert.Equal(t, *tc.expectedMergeResult.Message, *returnedResult.Message) assert.Equal(t, *tc.expectedMergeResult.SHA, *returnedResult.SHA) }) } } func Test_SearchPullRequests(t *testing.T) { serverTool := SearchPullRequests(translations.NullTranslationHelper) tool := serverTool.Tool // SearchPullRequests is the FeatureFlagFieldsParam-enabled variant; it owns // the _ff_ snapshot. The canonical search_pull_requests.snap is owned // by LegacySearchPullRequests (see Test_LegacySearchPullRequests_Definition). require.NoError(t, toolsnaps.Test(tool.Name+"_ff_"+FeatureFlagFieldsParam, tool)) require.Equal(t, FeatureFlagFieldsParam, serverTool.FeatureFlagEnable) assert.Equal(t, "search_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "query") assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "order") assert.Contains(t, schema.Properties, "perPage") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "fields") assert.ElementsMatch(t, schema.Required, []string{"query"}) mockSearchResult := &github.IssuesSearchResult{ Total: github.Ptr(2), IncompleteResults: github.Ptr(false), Issues: []*github.Issue{ { Number: github.Ptr(42), Title: github.Ptr(baselineUnsafeText), Body: github.Ptr(baselineUnsafeText), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/1"), Comments: github.Ptr(5), Labels: []*github.Label{{ Name: github.Ptr(baselineUnsafeText), Description: github.Ptr(baselineUnsafeText), }}, Milestone: &github.Milestone{ Title: github.Ptr(baselineUnsafeText), Description: github.Ptr(baselineUnsafeText), }, User: &github.User{ Login: github.Ptr("user1"), }, }, { Number: github.Ptr(43), Title: github.Ptr("Test PR 2"), Body: github.Ptr("Updated build scripts."), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/2"), Comments: github.Ptr(3), User: &github.User{ Login: github.Ptr("user2"), }, }, }, } expectedSearchResult := sanitizedIssuesSearchResultCopy(mockSearchResult) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedResult *github.IssuesSearchResult expectedErrMsg string }{ { name: "successful pull request search with all parameters", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: expectQueryParams( t, map[string]string{ "q": "is:pr repo:owner/repo is:open", "sort": "created", "order": "desc", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "repo:owner/repo is:open", "sort": "created", "order": "desc", "page": float64(1), "perPage": float64(30), }, expectError: false, expectedResult: expectedSearchResult, }, { name: "pull request search with owner and repo parameters", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: expectQueryParams( t, map[string]string{ "q": "repo:test-owner/test-repo is:pr draft:false", "sort": "updated", "order": "asc", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "draft:false", "owner": "test-owner", "repo": "test-repo", "sort": "updated", "order": "asc", }, expectError: false, expectedResult: expectedSearchResult, }, { name: "pull request search with only owner parameter (should ignore it)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: expectQueryParams( t, map[string]string{ "q": "is:pr feature", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "feature", "owner": "test-owner", }, expectError: false, expectedResult: expectedSearchResult, }, { name: "pull request search with only repo parameter (should ignore it)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: expectQueryParams( t, map[string]string{ "q": "is:pr review-required", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "review-required", "repo": "test-repo", }, expectError: false, expectedResult: expectedSearchResult, }, { name: "pull request search with minimal parameters", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: mockResponse(t, http.StatusOK, mockSearchResult), }), requestArgs: map[string]any{ "query": "is:pr repo:owner/repo is:open", }, expectError: false, expectedResult: expectedSearchResult, }, { name: "query with existing is:pr filter - no duplication", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: expectQueryParams( t, map[string]string{ "q": "is:pr repo:github/github-mcp-server is:open draft:false", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "is:pr repo:github/github-mcp-server is:open draft:false", }, expectError: false, expectedResult: expectedSearchResult, }, { name: "query with existing repo: filter and conflicting owner/repo params - uses query filter", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: expectQueryParams( t, map[string]string{ "q": "is:pr repo:github/github-mcp-server author:octocat", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "repo:github/github-mcp-server author:octocat", "owner": "different-owner", "repo": "different-repo", }, expectError: false, expectedResult: expectedSearchResult, }, { name: "complex query with existing is:pr filter and OR operators", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: expectQueryParams( t, map[string]string{ "q": "is:pr repo:github/github-mcp-server (label:bug OR label:enhancement OR label:feature)", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "is:pr repo:github/github-mcp-server (label:bug OR label:enhancement OR label:feature)", }, expectError: false, expectedResult: expectedSearchResult, }, { name: "search pull requests fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchIssues: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }, }), requestArgs: map[string]any{ "query": "invalid:query", }, expectError: true, expectedErrMsg: "failed to search pull requests", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := mustNewGHClient(t, tc.mockedClient) serverTool := SearchPullRequests(translations.NullTranslationHelper) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &request) // Verify results if tc.expectError { require.NoError(t, err) require.NotNil(t, result) require.True(t, result.IsError) textContent := getErrorResult(t, result) assert.Contains(t, textContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult github.IssuesSearchResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total) assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults) assert.Len(t, returnedResult.Issues, len(tc.expectedResult.Issues)) for i, issue := range returnedResult.Issues { assert.Equal(t, *tc.expectedResult.Issues[i].Number, *issue.Number) assert.Equal(t, *tc.expectedResult.Issues[i].Title, *issue.Title) assert.Equal(t, *tc.expectedResult.Issues[i].Body, *issue.Body) assert.Equal(t, *tc.expectedResult.Issues[i].State, *issue.State) assert.Equal(t, *tc.expectedResult.Issues[i].HTMLURL, *issue.HTMLURL) assert.Equal(t, *tc.expectedResult.Issues[i].User.Login, *issue.User.Login) if len(tc.expectedResult.Issues[i].Labels) > 0 { assert.Equal(t, tc.expectedResult.Issues[i].Labels[0].GetName(), issue.Labels[0].GetName()) assert.Equal(t, tc.expectedResult.Issues[i].Labels[0].GetDescription(), issue.Labels[0].GetDescription()) } if tc.expectedResult.Issues[i].Milestone != nil { assert.Equal(t, tc.expectedResult.Issues[i].Milestone.GetTitle(), issue.Milestone.GetTitle()) assert.Equal(t, tc.expectedResult.Issues[i].Milestone.GetDescription(), issue.Milestone.GetDescription()) } } assert.Equal(t, baselineUnsafeText, mockSearchResult.Issues[0].GetTitle()) assert.Equal(t, baselineUnsafeText, mockSearchResult.Issues[0].GetBody()) assert.Equal(t, baselineUnsafeText, mockSearchResult.Issues[0].Labels[0].GetName()) }) } } func Test_GetPullRequestFiles(t *testing.T) { // Verify tool definition once serverTool := PullRequestRead(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "method") assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "pullNumber") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "perPage") assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR files for success case mockFiles := []*github.CommitFile{ { Filename: github.Ptr("file1.go"), Status: github.Ptr("modified"), Additions: github.Ptr(10), Deletions: github.Ptr(5), Changes: github.Ptr(15), Patch: github.Ptr("@@ -1,5 +1,10 @@"), }, { Filename: github.Ptr("file2.go"), Status: github.Ptr("added"), Additions: github.Ptr(20), Deletions: github.Ptr(0), Changes: github.Ptr(20), Patch: github.Ptr("@@ -0,0 +1,20 @@"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedFiles []*github.CommitFile expectedErrMsg string lockdownEnabled bool restPermission string }{ { name: "successful files fetch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsFilesByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{ "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockFiles), ), }), requestArgs: map[string]any{ "method": "get_files", "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedFiles: mockFiles, }, { name: "successful files fetch with pagination", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsFilesByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{ "page": "2", "per_page": "10", }).andThen( mockResponse(t, http.StatusOK, mockFiles), ), }), requestArgs: map[string]any{ "method": "get_files", "owner": "owner", "repo": "repo", "pullNumber": float64(42), "page": float64(2), "perPage": float64(10), }, expectError: false, expectedFiles: mockFiles, }, { name: "files fetch fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsFilesByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{ "page": "1", "per_page": "30", }).andThen( http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), }), requestArgs: map[string]any{ "method": "get_files", "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request files", }, { name: "lockdown enabled - author lacks push access", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &github.PullRequest{ Number: github.Ptr(42), User: &github.User{Login: github.Ptr("reader")}, }), }), requestArgs: map[string]any{ "method": "get_files", "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, lockdownEnabled: true, restPermission: "read", expectError: true, expectedErrMsg: "access to pull request is restricted by lockdown mode", }, { name: "lockdown enabled - author has push access", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &github.PullRequest{ Number: github.Ptr(42), User: &github.User{Login: github.Ptr("writer")}, }), GetReposPullsFilesByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockFiles), }), requestArgs: map[string]any{ "method": "get_files", "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, lockdownEnabled: true, restPermission: "write", expectError: false, expectedFiles: mockFiles, }, { name: "lockdown enabled - pull request fetch fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsByOwnerByRepoByPullNumber: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), }), requestArgs: map[string]any{ "method": "get_files", "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, lockdownEnabled: true, restPermission: "read", expectError: true, expectedErrMsg: "failed to get pull request", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := mustNewGHClient(t, tc.mockedClient) serverTool := PullRequestRead(translations.NullTranslationHelper) var restClient *github.Client if tc.lockdownEnabled { restClient = mockRESTPermissionServer(t, tc.restPermission, nil) } deps := BaseDeps{ Client: client, RepoAccessCache: stubRepoAccessCache(restClient, 5*time.Minute), Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled}), } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedFiles []MinimalPRFile err = json.Unmarshal([]byte(textContent.Text), &returnedFiles) require.NoError(t, err) assert.Len(t, returnedFiles, len(tc.expectedFiles)) for i, file := range returnedFiles { assert.Equal(t, tc.expectedFiles[i].GetFilename(), file.Filename) assert.Equal(t, tc.expectedFiles[i].GetStatus(), file.Status) assert.Equal(t, tc.expectedFiles[i].GetAdditions(), file.Additions) assert.Equal(t, tc.expectedFiles[i].GetDeletions(), file.Deletions) } }) } } func Test_GetPullRequestCommits(t *testing.T) { // Verify tool definition once serverTool := PullRequestRead(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "method") assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "pullNumber") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "perPage") assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) authorDate := time.Date(2026, 4, 1, 12, 0, 0, 0, time.UTC) mockCommits := []*github.RepositoryCommit{ { SHA: github.Ptr("abc123def456"), HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"), Commit: &github.Commit{ Message: github.Ptr(baselineUnsafeText), Author: &github.CommitAuthor{ Name: github.Ptr(baselineUnsafeText), Email: github.Ptr("test@example.com"), Date: &github.Timestamp{Time: authorDate}, }, Committer: &github.CommitAuthor{ Name: github.Ptr("Merge Bot"), Email: github.Ptr("merge@example.com"), Date: &github.Timestamp{Time: authorDate.Add(30 * time.Minute)}, }, }, Author: &github.User{ Login: github.Ptr("test-user"), ID: github.Ptr(int64(12345)), HTMLURL: github.Ptr("https://github.com/test-user"), AvatarURL: github.Ptr("https://github.com/test-user.png"), }, Committer: &github.User{ Login: github.Ptr("merge-bot"), ID: github.Ptr(int64(67890)), HTMLURL: github.Ptr("https://github.com/merge-bot"), AvatarURL: github.Ptr("https://github.com/merge-bot.png"), }, }, { SHA: github.Ptr("def456abc789"), HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456abc789"), Commit: &github.Commit{ Message: github.Ptr("fix: handle pagination"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedCommits []*github.RepositoryCommit expectedErrMsg string }{ { name: "successful commits fetch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsCommitsByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{ "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockCommits), ), }), requestArgs: map[string]any{ "method": "get_commits", "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedCommits: mockCommits, }, { name: "successful commits fetch with pagination", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsCommitsByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{ "page": "2", "per_page": "10", }).andThen( mockResponse(t, http.StatusOK, mockCommits), ), }), requestArgs: map[string]any{ "method": "get_commits", "owner": "owner", "repo": "repo", "pullNumber": float64(42), "page": float64(2), "perPage": float64(10), }, expectError: false, expectedCommits: mockCommits, }, { name: "commits fetch fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposPullsCommitsByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{ "page": "1", "per_page": "30", }).andThen( http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), }), requestArgs: map[string]any{ "method": "get_commits", "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request commits", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := mustNewGHClient(t, tc.mockedClient) serverTool := PullRequestRead(translations.NullTranslationHelper) deps := BaseDeps{ Client: client, RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute), Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}), } handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) result, err := handler(ContextWithDeps(context.Background(), deps), &request) if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) assert.NotContains(t, textContent.Text, `"committer"`) assert.NotContains(t, textContent.Text, `"profile_url"`) var returnedCommits []MinimalPullRequestCommit err = json.Unmarshal([]byte(textContent.Text), &returnedCommits) require.NoError(t, err) assert.Len(t, returnedCommits, len(tc.expectedCommits)) for i, commit := range returnedCommits { assert.Equal(t, tc.expectedCommits[i].GetSHA(), commit.SHA) assert.Equal(t, tc.expectedCommits[i].GetHTMLURL(), commit.HTMLURL) assert.Equal(t, sanitizeCommitMessage(tc.expectedCommits[i].GetCommit().GetMessage()), commit.Message) } assert.Equal(t, sanitizeOutputText(baselineUnsafeText), returnedCommits[0].Author.Name) assert.Equal(t, authorDate.Format(time.RFC3339), returnedCommits[0].Author.Date) assert.Equal(t, baselineUnsafeText, mockCommits[0].GetCommit().GetMessage()) }) } } func Test_ConvertToMinimalPullRequestCommitsSkipsNilCommit(t *testing.T) { commits := convertToMinimalPullRequestCommits([]*github.RepositoryCommit{nil}) require.Empty(t, commits) } func Test_GetPullRequestStatus(t *testing.T) { // Verify tool definition once serverTool := PullRequestRead(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) schema := tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, schema.Properties, "method") assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "pullNumber") assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR for successful PR fetch mockPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Head: &github.PullRequestBranch{ SHA: github.Ptr("abcd1234"), Ref: github.Ptr("feature-branch"), }, } // Setup mock status for success case mockStatus := &github.CombinedStatus{ State: github.Ptr("success"), TotalCount: github.Ptr(3), Statuses: []*github.RepoStatus{ { State: github.Ptr("success"), Context: github.Ptr("continuous-integration/