Files
github--github-mcp-server/pkg/github/granular_tools_test.go
Copilot 569a48d847
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
GoReleaser Release / release (push) Has been cancelled
MCP Server Diff / mcp-diff (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
Enforce exactly one value key per field in set_issue_fields (#2339)
* Initial plan

* Enforce exactly one value key per field in set_issue_fields and add tests

Address review feedback:
- Change validation to count value keys and reject when multiple are
  provided (e.g., text_value + number_value, or text_value + delete).
- Add unit tests for multiple value keys and value + delete scenarios.
- Run generate-docs (no doc changes needed; README was already current).

Agent-Logs-Url: https://github.com/github/github-mcp-server/sessions/7e89edb3-5315-42dd-bfa1-6c962f1ba137

Co-authored-by: mattdholloway <918573+mattdholloway@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mattdholloway <918573+mattdholloway@users.noreply.github.com>
2026-04-16 15:11:24 +02:00

949 lines
29 KiB
Go

package github
import (
"context"
"net/http"
"testing"
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v82/github"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func granularToolsForToolset(toolsetID inventory.ToolsetID, featureFlag string) []inventory.ServerTool {
var result []inventory.ServerTool
for _, tool := range AllTools(translations.NullTranslationHelper) {
if tool.Toolset.ID == toolsetID && tool.FeatureFlagEnable == featureFlag {
result = append(result, tool)
}
}
return result
}
func TestGranularToolSnaps(t *testing.T) {
// Test toolsnaps for all granular tools
toolConstructors := []func(translations.TranslationHelperFunc) inventory.ServerTool{
GranularCreateIssue,
GranularUpdateIssueTitle,
GranularUpdateIssueBody,
GranularUpdateIssueAssignees,
GranularUpdateIssueLabels,
GranularUpdateIssueMilestone,
GranularUpdateIssueType,
GranularUpdateIssueState,
GranularAddSubIssue,
GranularRemoveSubIssue,
GranularReprioritizeSubIssue,
GranularSetIssueFields,
GranularUpdatePullRequestTitle,
GranularUpdatePullRequestBody,
GranularUpdatePullRequestState,
GranularUpdatePullRequestDraftState,
GranularRequestPullRequestReviewers,
GranularCreatePullRequestReview,
GranularSubmitPendingPullRequestReview,
GranularDeletePendingPullRequestReview,
GranularAddPullRequestReviewComment,
GranularResolveReviewThread,
GranularUnresolveReviewThread,
}
for _, constructor := range toolConstructors {
serverTool := constructor(translations.NullTranslationHelper)
t.Run(serverTool.Tool.Name, func(t *testing.T) {
require.NoError(t, toolsnaps.Test(serverTool.Tool.Name, serverTool.Tool))
})
}
}
func TestIssuesGranularToolset(t *testing.T) {
t.Run("toolset contains expected granular tools", func(t *testing.T) {
tools := granularToolsForToolset(ToolsetMetadataIssues.ID, FeatureFlagIssuesGranular)
toolNames := make([]string, 0, len(tools))
for _, tool := range tools {
toolNames = append(toolNames, tool.Tool.Name)
}
expected := []string{
"create_issue",
"update_issue_title",
"update_issue_body",
"update_issue_assignees",
"update_issue_labels",
"update_issue_milestone",
"update_issue_type",
"update_issue_state",
"add_sub_issue",
"remove_sub_issue",
"reprioritize_sub_issue",
"set_issue_fields",
}
for _, name := range expected {
assert.Contains(t, toolNames, name)
}
assert.Len(t, tools, len(expected))
})
t.Run("all granular tools have correct feature flag", func(t *testing.T) {
for _, tool := range granularToolsForToolset(ToolsetMetadataIssues.ID, FeatureFlagIssuesGranular) {
assert.Equal(t, FeatureFlagIssuesGranular, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
}
})
}
func TestPullRequestsGranularToolset(t *testing.T) {
t.Run("toolset contains expected granular tools", func(t *testing.T) {
tools := granularToolsForToolset(ToolsetMetadataPullRequests.ID, FeatureFlagPullRequestsGranular)
toolNames := make([]string, 0, len(tools))
for _, tool := range tools {
toolNames = append(toolNames, tool.Tool.Name)
}
expected := []string{
"update_pull_request_title",
"update_pull_request_body",
"update_pull_request_state",
"update_pull_request_draft_state",
"request_pull_request_reviewers",
"create_pull_request_review",
"submit_pending_pull_request_review",
"delete_pending_pull_request_review",
"add_pull_request_review_comment",
"resolve_review_thread",
"unresolve_review_thread",
}
for _, name := range expected {
assert.Contains(t, toolNames, name)
}
assert.Len(t, tools, len(expected))
})
t.Run("all granular tools have correct feature flag", func(t *testing.T) {
for _, tool := range granularToolsForToolset(ToolsetMetadataPullRequests.ID, FeatureFlagPullRequestsGranular) {
assert.Equal(t, FeatureFlagPullRequestsGranular, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
}
})
}
// --- Issue granular tool handler tests ---
func TestGranularCreateIssue(t *testing.T) {
mockIssue := &gogithub.Issue{
Number: gogithub.Ptr(1),
Title: gogithub.Ptr("Test Issue"),
Body: gogithub.Ptr("Test body"),
}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectedErrMsg string
}{
{
name: "successful creation",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{
"title": "Test Issue",
"body": "Test body",
}).andThen(mockResponse(t, http.StatusCreated, mockIssue)),
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"title": "Test Issue",
"body": "Test body",
},
},
{
name: "missing required parameter",
mockedClient: MockHTTPClientWithHandlers(nil),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
},
expectedErrMsg: "missing required parameter: title",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := gogithub.NewClient(tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := GranularCreateIssue(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectedErrMsg != "" {
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
return
}
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueTitle(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, &gogithub.Issue{
Number: gogithub.Ptr(42),
Title: gogithub.Ptr("New Title"),
}),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdateIssueTitle(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(42),
"title": "New Title",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdateIssueBody(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"body": "Updated body",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{
Number: gogithub.Ptr(1),
Body: gogithub.Ptr("Updated body"),
})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdateIssueBody(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"body": "Updated body",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdateIssueAssignees(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"assignees": []any{"user1", "user2"},
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdateIssueAssignees(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []string{"user1", "user2"},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdateIssueLabels(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"labels": []any{"bug", "enhancement"},
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []string{"bug", "enhancement"},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdateIssueMilestone(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"milestone": float64(5),
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdateIssueMilestone(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"milestone": float64(5),
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdateIssueType(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"type": "bug",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdateIssueState(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "close with reason",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"state_reason": "completed",
},
expectedReq: map[string]any{
"state": "closed",
"state_reason": "completed",
},
},
{
name: "reopen without reason",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "open",
},
expectedReq: map[string]any{
"state": "open",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{
Number: gogithub.Ptr(1),
State: gogithub.Ptr(tc.requestArgs["state"].(string)),
})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
// --- Pull request granular tool handler tests ---
func TestGranularUpdatePullRequestTitle(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
"title": "New PR Title",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
Number: gogithub.Ptr(1),
Title: gogithub.Ptr("New PR Title"),
})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdatePullRequestTitle(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(1),
"title": "New PR Title",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdatePullRequestBody(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
"body": "Updated description",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
Number: gogithub.Ptr(1),
Body: gogithub.Ptr("Updated description"),
})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdatePullRequestBody(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(1),
"body": "Updated description",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdatePullRequestState(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
"state": "closed",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
Number: gogithub.Ptr(1),
State: gogithub.Ptr("closed"),
})),
}))
deps := BaseDeps{Client: client}
serverTool := GranularUpdatePullRequestState(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(1),
"state": "closed",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularRequestPullRequestReviewers(t *testing.T) {
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &gogithub.PullRequest{Number: gogithub.Ptr(1)}),
}))
deps := BaseDeps{Client: client}
serverTool := GranularRequestPullRequestReviewers(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(1),
"reviewers": []string{"user1", "user2"},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularCreatePullRequestReview(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
PullRequest struct {
ID githubv4.ID
} `graphql:"pullRequest(number: $prNum)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"prNum": githubv4.Int(1),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"pullRequest": map[string]any{
"id": "PR_123",
},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
AddPullRequestReview struct {
PullRequestReview struct {
ID githubv4.ID
}
} `graphql:"addPullRequestReview(input: $input)"`
}{},
githubv4.AddPullRequestReviewInput{
PullRequestID: githubv4.ID("PR_123"),
Body: githubv4.NewString("LGTM"),
Event: githubv4mock.Ptr(githubv4.PullRequestReviewEventApprove),
},
nil,
githubv4mock.DataResponse(map[string]any{}),
),
)
gqlClient := githubv4.NewClient(mockedClient)
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularCreatePullRequestReview(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(1),
"body": "LGTM",
"event": "APPROVE",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUpdatePullRequestDraftState(t *testing.T) {
tests := []struct {
name string
draft bool
}{
{name: "convert to draft", draft: true},
{name: "mark ready for review", draft: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var matchers []githubv4mock.Matcher
matchers = append(matchers, githubv4mock.NewQueryMatcher(
struct {
Repository struct {
PullRequest struct {
ID githubv4.ID
} `graphql:"pullRequest(number: $number)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"name": githubv4.String("repo"),
"number": githubv4.Int(1),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"pullRequest": map[string]any{"id": "PR_123"},
},
}),
))
if tc.draft {
matchers = append(matchers, githubv4mock.NewMutationMatcher(
struct {
ConvertPullRequestToDraft struct {
PullRequest struct {
ID githubv4.ID
IsDraft githubv4.Boolean
}
} `graphql:"convertPullRequestToDraft(input: $input)"`
}{},
githubv4.ConvertPullRequestToDraftInput{PullRequestID: githubv4.ID("PR_123")},
nil,
githubv4mock.DataResponse(map[string]any{
"convertPullRequestToDraft": map[string]any{
"pullRequest": map[string]any{"id": "PR_123", "isDraft": true},
},
}),
))
} else {
matchers = append(matchers, githubv4mock.NewMutationMatcher(
struct {
MarkPullRequestReadyForReview struct {
PullRequest struct {
ID githubv4.ID
IsDraft githubv4.Boolean
}
} `graphql:"markPullRequestReadyForReview(input: $input)"`
}{},
githubv4.MarkPullRequestReadyForReviewInput{PullRequestID: githubv4.ID("PR_123")},
nil,
githubv4mock.DataResponse(map[string]any{
"markPullRequestReadyForReview": map[string]any{
"pullRequest": map[string]any{"id": "PR_123", "isDraft": false},
},
}),
))
}
gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularUpdatePullRequestDraftState(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(1),
"draft": tc.draft,
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularAddPullRequestReviewComment(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Viewer struct {
Login githubv4.String
}
}{},
nil,
githubv4mock.DataResponse(map[string]any{
"viewer": map[string]any{"login": "testuser"},
}),
),
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
PullRequest struct {
Reviews struct {
Nodes []struct {
ID githubv4.ID
State githubv4.PullRequestReviewState
URL githubv4.URI
}
} `graphql:"reviews(first: 1, author: $author)"`
} `graphql:"pullRequest(number: $prNum)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}{},
map[string]any{
"author": githubv4.String("testuser"),
"owner": githubv4.String("owner"),
"name": githubv4.String("repo"),
"prNum": githubv4.Int(1),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"pullRequest": map[string]any{
"reviews": map[string]any{
"nodes": []map[string]any{
{"id": "PRR_123", "state": "PENDING", "url": "https://github.com/owner/repo/pull/1#pullrequestreview-123"},
},
},
},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
AddPullRequestReviewThread struct {
Thread struct {
ID githubv4.ID
}
} `graphql:"addPullRequestReviewThread(input: $input)"`
}{},
githubv4.AddPullRequestReviewThreadInput{
Path: githubv4.String("src/main.go"),
Body: githubv4.String("This needs a fix"),
SubjectType: githubv4mock.Ptr(githubv4.PullRequestReviewThreadSubjectTypeLine),
Line: githubv4mock.Ptr(githubv4.Int(42)),
Side: githubv4mock.Ptr(githubv4.DiffSideRight),
PullRequestReviewID: githubv4mock.Ptr(githubv4.ID("PRR_123")),
},
nil,
githubv4mock.DataResponse(map[string]any{
"addPullRequestReviewThread": map[string]any{
"thread": map[string]any{"id": "PRRT_456"},
},
}),
),
)
gqlClient := githubv4.NewClient(mockedClient)
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularAddPullRequestReviewComment(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(1),
"path": "src/main.go",
"body": "This needs a fix",
"subjectType": "LINE",
"line": float64(42),
"side": "RIGHT",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularResolveReviewThread(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewMutationMatcher(
struct {
ResolveReviewThread struct {
Thread struct {
ID githubv4.ID
IsResolved githubv4.Boolean
}
} `graphql:"resolveReviewThread(input: $input)"`
}{},
githubv4.ResolveReviewThreadInput{
ThreadID: githubv4.ID("PRRT_123"),
},
nil,
githubv4mock.DataResponse(map[string]any{
"resolveReviewThread": map[string]any{
"thread": map[string]any{"id": "PRRT_123", "isResolved": true},
},
}),
),
)
gqlClient := githubv4.NewClient(mockedClient)
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularResolveReviewThread(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"threadID": "PRRT_123",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularUnresolveReviewThread(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewMutationMatcher(
struct {
UnresolveReviewThread struct {
Thread struct {
ID githubv4.ID
IsResolved githubv4.Boolean
}
} `graphql:"unresolveReviewThread(input: $input)"`
}{},
githubv4.UnresolveReviewThreadInput{
ThreadID: githubv4.ID("PRRT_123"),
},
nil,
githubv4mock.DataResponse(map[string]any{
"unresolveReviewThread": map[string]any{
"thread": map[string]any{"id": "PRRT_123", "isResolved": false},
},
}),
),
)
gqlClient := githubv4.NewClient(mockedClient)
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularUnresolveReviewThread(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"threadID": "PRRT_123",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
}
func TestGranularSetIssueFields(t *testing.T) {
t.Run("successful set with text value", func(t *testing.T) {
matchers := []githubv4mock.Matcher{
// Mock the issue ID query
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Issue struct {
ID githubv4.ID
} `graphql:"issue(number: $issueNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"issueNumber": githubv4.Int(5),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"issue": map[string]any{"id": "ISSUE_123"},
},
}),
),
// Mock the setIssueFieldValue mutation
githubv4mock.NewMutationMatcher(
struct {
SetIssueFieldValue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
IssueFieldValues []struct {
Field struct {
Name string
} `graphql:"... on IssueFieldDateValue"`
}
} `graphql:"setIssueFieldValue(input: $input)"`
}{},
SetIssueFieldValueInput{
IssueID: githubv4.ID("ISSUE_123"),
IssueFields: []IssueFieldCreateOrUpdateInput{
{
FieldID: githubv4.ID("FIELD_1"),
TextValue: githubv4.NewString(githubv4.String("hello")),
},
},
},
nil,
githubv4mock.DataResponse(map[string]any{
"setIssueFieldValue": map[string]any{
"issue": map[string]any{
"id": "ISSUE_123",
"number": 5,
"url": "https://github.com/owner/repo/issues/5",
},
},
}),
),
}
gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(5),
"fields": []any{
map[string]any{"field_id": "FIELD_1", "text_value": "hello"},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
t.Run("missing required parameter fields", func(t *testing.T) {
deps := BaseDeps{}
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(5),
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "missing required parameter: fields")
})
t.Run("empty fields array", func(t *testing.T) {
deps := BaseDeps{}
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(5),
"fields": []any{},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "fields array must not be empty")
})
t.Run("field missing value", func(t *testing.T) {
deps := BaseDeps{}
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(5),
"fields": []any{
map[string]any{"field_id": "FIELD_1"},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "each field must have a value")
})
t.Run("multiple value keys returns error", func(t *testing.T) {
deps := BaseDeps{}
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(5),
"fields": []any{
map[string]any{"field_id": "FIELD_1", "text_value": "hello", "number_value": float64(42)},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "each field must have exactly one value")
})
t.Run("value key with delete returns error", func(t *testing.T) {
deps := BaseDeps{}
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(5),
"fields": []any{
map[string]any{"field_id": "FIELD_1", "text_value": "hello", "delete": true},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "each field must have exactly one value")
})
}