Files
2026-07-13 12:37:57 +08:00

2626 lines
77 KiB
Go

package github
import (
"context"
"encoding/json"
"net/http"
"strings"
"testing"
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/http/headers"
transportpkg "github.com/github/github-mcp-server/pkg/http/transport"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v89/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,
GranularAddIssueReaction,
GranularAddIssueCommentReaction,
GranularUpdatePullRequestTitle,
GranularUpdatePullRequestBody,
GranularUpdatePullRequestState,
GranularUpdatePullRequestDraftState,
GranularRequestPullRequestReviewers,
GranularCreatePullRequestReview,
GranularSubmitPendingPullRequestReview,
GranularDeletePendingPullRequestReview,
GranularAddPullRequestReviewComment,
GranularResolveReviewThread,
GranularUnresolveReviewThread,
GranularAddPullRequestReviewCommentReaction,
}
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",
"add_issue_reaction",
"add_issue_comment_reaction",
}
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",
"add_pull_request_review_comment_reaction",
}
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 := mustNewGHClient(t, 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 := mustNewGHClient(t, 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 := mustNewGHClient(t, 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 := mustNewGHClient(t, 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 TestGranularUpdateIssueAssigneesObjectForm(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "assignee objects without intent serialize as strings",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{
map[string]any{"login": "octocat"},
"monalisa",
},
},
expectedReq: map[string]any{
"assignees": []any{"octocat", "monalisa"},
},
},
{
name: "assignee suggested without rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{
map[string]any{"login": "octocat", "is_suggestion": true},
},
},
expectedReq: map[string]any{
"assignees": []any{
map[string]any{"login": "octocat", "suggest": true},
},
},
},
{
name: "suggested assignee with rationale and confidence",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{
map[string]any{"login": "octocat", "rationale": " Authored the crashing file ", "confidence": "high", "is_suggestion": true},
},
},
expectedReq: map[string]any{
"assignees": []any{
map[string]any{"login": "octocat", "rationale": "Authored the crashing file", "confidence": "HIGH", "suggest": true},
},
},
},
{
name: "mix of plain and suggested assignees",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{
"monalisa",
map[string]any{"login": "octocat", "is_suggestion": true},
},
},
expectedReq: map[string]any{
"assignees": []any{
"monalisa",
map[string]any{"login": "octocat", "suggest": true},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
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(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueAssigneesInvalidInput(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedErrText string
}{
{
name: "rationale too long",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{
map[string]any{"login": "octocat", "rationale": strings.Repeat("a", 281)},
},
},
expectedErrText: "assignee rationale must be 280 characters or less",
},
{
name: "assignee object missing login",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{
map[string]any{"rationale": "no login provided"},
},
},
expectedErrText: "each assignee object must have a 'login' string",
},
{
name: "invalid confidence value",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{
map[string]any{"login": "octocat", "confidence": "maybe"},
},
},
expectedErrText: "confidence must be one of: LOW, MEDIUM, HIGH",
},
{
name: "assignee entry is neither string nor object",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"assignees": []any{float64(123)},
},
expectedErrText: "each assignee must be a string or an object",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
serverTool := GranularUpdateIssueAssignees(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrText)
})
}
}
func TestGranularUpdateIssueLabels(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "labels as plain strings",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{"bug", "enhancement"},
},
expectedReq: map[string]any{
"labels": []any{"bug", "enhancement"},
},
},
{
name: "label objects without rationale serialize as strings",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "bug"},
"enhancement",
},
},
expectedReq: map[string]any{
"labels": []any{"bug", "enhancement"},
},
},
{
name: "mixed strings and label objects with rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
"triage",
map[string]any{"name": "bug", "rationale": " Reports a crash when saving "},
map[string]any{"name": "frontend", "rationale": "Mentions the UI button"},
},
},
expectedReq: map[string]any{
"labels": []any{
"triage",
map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
map[string]any{"name": "frontend", "rationale": "Mentions the UI button"},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
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(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueLabelsSuggest(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "single label suggested without rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "bug", "is_suggestion": true},
},
},
expectedReq: map[string]any{
"labels": []any{
map[string]any{"name": "bug", "suggest": true},
},
},
},
{
name: "suggested label with rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "frontend", "rationale": "Mentions the UI button", "is_suggestion": true},
},
},
expectedReq: map[string]any{
"labels": []any{
map[string]any{"name": "frontend", "rationale": "Mentions the UI button", "suggest": true},
},
},
},
{
name: "mix of plain, applied-with-rationale, and suggested labels",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
"triage",
map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
map[string]any{"name": "needs-design", "is_suggestion": true},
},
},
expectedReq: map[string]any{
"labels": []any{
"triage",
map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
map[string]any{"name": "needs-design", "suggest": true},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
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(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueLabelsInvalidRationale(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedErrText string
}{
{
name: "rationale too long",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "bug", "rationale": strings.Repeat("a", 281)},
},
},
expectedErrText: "label rationale must be 280 characters or less",
},
{
name: "label object missing name",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"rationale": "no name provided"},
},
},
expectedErrText: "each label object must have a 'name' string",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrText)
})
}
}
func TestGranularUpdateIssueLabelsConfidence(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "label with confidence triggers object form",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "bug", "confidence": "HIGH"},
},
},
expectedReq: map[string]any{
"labels": []any{
map[string]any{"name": "bug", "confidence": "HIGH"},
},
},
},
{
name: "label with confidence and rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "bug", "rationale": "Reports a crash", "confidence": "MEDIUM"},
},
},
expectedReq: map[string]any{
"labels": []any{
map[string]any{"name": "bug", "rationale": "Reports a crash", "confidence": "MEDIUM"},
},
},
},
{
name: "label confidence is normalized",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "bug", "confidence": " high\t"},
},
},
expectedReq: map[string]any{
"labels": []any{
map[string]any{"name": "bug", "confidence": "HIGH"},
},
},
},
{
name: "invalid confidence value",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"labels": []any{
map[string]any{"name": "bug", "confidence": "very_high"},
},
},
expectedReq: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.expectedReq == nil {
// Error case
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, "confidence must be one of: LOW, MEDIUM, HIGH")
return
}
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
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(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueMilestone(t *testing.T) {
client := mustNewGHClient(t, 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) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "type only",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
},
expectedReq: map[string]any{
"type": "bug",
},
},
{
name: "type with rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "feature",
"rationale": " This issue requests a new capability ",
},
expectedReq: map[string]any{
"type": map[string]any{
"value": "feature",
"rationale": "This issue requests a new capability",
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
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(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueTypeSuggest(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "suggest without rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
"is_suggestion": true,
},
expectedReq: map[string]any{
"type": map[string]any{
"value": "bug",
"suggest": true,
},
},
},
{
name: "suggest with rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "feature",
"rationale": " Asks for dark mode support ",
"is_suggestion": true,
},
expectedReq: map[string]any{
"type": map[string]any{
"value": "feature",
"rationale": "Asks for dark mode support",
"suggest": true,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
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(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueTypeInvalidRationale(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedErrText string
}{
{
name: "rationale wrong type",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "feature",
"rationale": float64(123),
},
expectedErrText: "parameter rationale is not of type string, is float64",
},
{
name: "rationale too long",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "feature",
"rationale": strings.Repeat("a", 281),
},
expectedErrText: "parameter rationale must be 280 characters or less",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrText)
})
}
}
func TestGranularUpdateIssueTypeConfidence(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "type with confidence only",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
"confidence": "HIGH",
},
expectedReq: map[string]any{
"type": map[string]any{
"value": "bug",
"confidence": "HIGH",
},
},
},
{
name: "type with confidence and rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "feature",
"rationale": "Asks for dark mode support",
"confidence": "MEDIUM",
},
expectedReq: map[string]any{
"type": map[string]any{
"value": "feature",
"rationale": "Asks for dark mode support",
"confidence": "MEDIUM",
},
},
},
{
name: "type with low confidence triggers object form",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
"confidence": "LOW",
},
expectedReq: map[string]any{
"type": map[string]any{
"value": "bug",
"confidence": "LOW",
},
},
},
{
name: "type confidence is normalized",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
"confidence": " medium ",
},
expectedReq: map[string]any{
"type": map[string]any{
"value": "bug",
"confidence": "MEDIUM",
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
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(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}
func TestGranularUpdateIssueTypeInvalidConfidence(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedErrText string
}{
{
name: "invalid confidence value",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
"confidence": "very_high",
},
expectedErrText: "confidence must be one of: LOW, MEDIUM, HIGH",
},
{
name: "confidence wrong type",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"issue_type": "bug",
"confidence": float64(85),
},
expectedErrText: "parameter confidence is not of type string",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrText)
})
}
}
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 := mustNewGHClient(t, 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)
})
}
}
func TestGranularUpdateIssueStateSuggest(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "suggest without rationale",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"is_suggestion": true,
},
expectedReq: map[string]any{
"state": map[string]any{
"value": "closed",
"suggest": true,
},
},
},
{
name: "suggest with rationale and state_reason",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"state_reason": "not_planned",
"rationale": " No activity in 6 months ",
"is_suggestion": true,
},
expectedReq: map[string]any{
"state": map[string]any{
"value": "closed",
"rationale": "No activity in 6 months",
"suggest": true,
},
"state_reason": "not_planned",
},
},
{
name: "rationale applied directly (no suggestion)",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"rationale": "The reported crash is fixed in v2.1",
"confidence": "HIGH",
},
expectedReq: map[string]any{
"state": map[string]any{
"value": "closed",
"rationale": "The reported crash is fixed in v2.1",
"confidence": "HIGH",
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
}))
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)
})
}
}
func TestGranularUpdateIssueStateDuplicate(t *testing.T) {
const duplicateIssueID = int64(99999)
tests := []struct {
name string
requestArgs map[string]any
expectedReq map[string]any
}{
{
name: "suggestion duplicate close",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"state_reason": "duplicate",
"is_suggestion": true,
"duplicate_of": float64(42),
},
expectedReq: map[string]any{
"state": map[string]any{
"value": "closed",
"suggest": true,
},
"state_reason": "duplicate",
"duplicate_issue_id": float64(duplicateIssueID),
},
},
{
name: "direct duplicate close",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"state_reason": "duplicate",
"duplicate_of": float64(42),
},
expectedReq: map[string]any{
"state": map[string]any{"value": "closed"},
"state_reason": "duplicate",
"duplicate_issue_id": float64(duplicateIssueID),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, &gogithub.Issue{
ID: gogithub.Ptr(duplicateIssueID),
Number: gogithub.Ptr(42),
}),
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
}))
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)
})
}
}
func TestGranularUpdateIssueStateInvalidRationale(t *testing.T) {
tests := []struct {
name string
requestArgs map[string]any
expectedErrText string
}{
{
name: "rationale too long",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"rationale": strings.Repeat("a", 281),
},
expectedErrText: "parameter rationale must be 280 characters or less",
},
{
name: "duplicate_of without state_reason duplicate",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"state_reason": "not_planned",
"is_suggestion": true,
"duplicate_of": float64(42),
},
expectedErrText: "duplicate_of can only be used when state_reason is 'duplicate'",
},
{
name: "suggestion duplicate without duplicate_of",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"state_reason": "duplicate",
"is_suggestion": true,
},
expectedErrText: "duplicate_of is required when suggesting a close as duplicate",
},
{
name: "state_reason with open state",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "open",
"state_reason": "completed",
},
expectedErrText: "state_reason can only be used when state is 'closed'",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrText)
})
}
}
func TestGranularUpdateIssueStateInvalidConfidence(t *testing.T) {
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(1),
"state": "closed",
"confidence": "VERY_HIGH",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, "confidence must be one of: LOW, MEDIUM, HIGH")
}
// --- Pull request granular tool handler tests ---
func TestGranularUpdatePullRequestTitle(t *testing.T) {
client := mustNewGHClient(t, 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 := mustNewGHClient(t, 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 := mustNewGHClient(t, 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 := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
"reviewers": []any{"user1"},
"team_reviewers": []any{"team1"},
}).andThen(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", "owner/team1"},
})
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 {
TextValue struct {
Value string
} `graphql:"... on IssueFieldTextValue"`
SingleSelectValue struct {
Name string
} `graphql:"... on IssueFieldSingleSelectValue"`
DateValue struct {
Value string
} `graphql:"... on IssueFieldDateValue"`
NumberValue struct {
Value float64
} `graphql:"... on IssueFieldNumberValue"`
}
} `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")
})
t.Run("successful set with text value and rationale", func(t *testing.T) {
matchers := []githubv4mock.Matcher{
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"},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
SetIssueFieldValue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
IssueFieldValues []struct {
TextValue struct {
Value string
} `graphql:"... on IssueFieldTextValue"`
SingleSelectValue struct {
Name string
} `graphql:"... on IssueFieldSingleSelectValue"`
DateValue struct {
Value string
} `graphql:"... on IssueFieldDateValue"`
NumberValue struct {
Value float64
} `graphql:"... on IssueFieldNumberValue"`
}
} `graphql:"setIssueFieldValue(input: $input)"`
}{},
SetIssueFieldValueInput{
IssueID: githubv4.ID("ISSUE_123"),
IssueFields: []IssueFieldCreateOrUpdateInput{
{
FieldID: githubv4.ID("FIELD_1"),
TextValue: githubv4.NewString(githubv4.String("hello")),
Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")),
},
},
},
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",
"rationale": " Reflects the reported severity ",
},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
t.Run("rationale too long 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",
"rationale": strings.Repeat("a", 281),
},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "field rationale must be 280 characters or less")
})
t.Run("successful set with confidence", func(t *testing.T) {
confidence := "HIGH"
matchers := []githubv4mock.Matcher{
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"},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
SetIssueFieldValue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
IssueFieldValues []struct {
TextValue struct {
Value string
} `graphql:"... on IssueFieldTextValue"`
SingleSelectValue struct {
Name string
} `graphql:"... on IssueFieldSingleSelectValue"`
DateValue struct {
Value string
} `graphql:"... on IssueFieldDateValue"`
NumberValue struct {
Value float64
} `graphql:"... on IssueFieldNumberValue"`
}
} `graphql:"setIssueFieldValue(input: $input)"`
}{},
SetIssueFieldValueInput{
IssueID: githubv4.ID("ISSUE_123"),
IssueFields: []IssueFieldCreateOrUpdateInput{
{
FieldID: githubv4.ID("FIELD_1"),
TextValue: githubv4.NewString(githubv4.String("hello")),
Confidence: &confidence,
},
},
},
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",
"confidence": " high ",
},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
t.Run("invalid confidence value 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",
"confidence": "very_high",
},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "confidence must be one of: LOW, MEDIUM, HIGH")
})
t.Run("confidence is sent when supplied", func(t *testing.T) {
confidence := "HIGH"
matchers := []githubv4mock.Matcher{
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"},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
SetIssueFieldValue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
IssueFieldValues []struct {
TextValue struct {
Value string
} `graphql:"... on IssueFieldTextValue"`
SingleSelectValue struct {
Name string
} `graphql:"... on IssueFieldSingleSelectValue"`
DateValue struct {
Value string
} `graphql:"... on IssueFieldDateValue"`
NumberValue struct {
Value float64
} `graphql:"... on IssueFieldNumberValue"`
}
} `graphql:"setIssueFieldValue(input: $input)"`
}{},
SetIssueFieldValueInput{
IssueID: githubv4.ID("ISSUE_123"),
IssueFields: []IssueFieldCreateOrUpdateInput{
{
FieldID: githubv4.ID("FIELD_1"),
TextValue: githubv4.NewString(githubv4.String("hello")),
Confidence: &confidence,
},
},
},
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",
"confidence": "HIGH",
},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError, getTextResult(t, result).Text)
})
t.Run("successful set with suggest flag", func(t *testing.T) {
suggestTrue := githubv4.Boolean(true)
matchers := []githubv4mock.Matcher{
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"},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
SetIssueFieldValue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
IssueFieldValues []struct {
TextValue struct {
Value string
} `graphql:"... on IssueFieldTextValue"`
SingleSelectValue struct {
Name string
} `graphql:"... on IssueFieldSingleSelectValue"`
DateValue struct {
Value string
} `graphql:"... on IssueFieldDateValue"`
NumberValue struct {
Value float64
} `graphql:"... on IssueFieldNumberValue"`
}
} `graphql:"setIssueFieldValue(input: $input)"`
}{},
SetIssueFieldValueInput{
IssueID: githubv4.ID("ISSUE_123"),
IssueFields: []IssueFieldCreateOrUpdateInput{
{
FieldID: githubv4.ID("FIELD_1"),
TextValue: githubv4.NewString(githubv4.String("hello")),
Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")),
Suggest: &suggestTrue,
},
},
},
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",
"rationale": "Reflects the reported severity",
"is_suggestion": true,
},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
t.Run("sends GraphQL-Features: update_issue_suggestions header on mutation", func(t *testing.T) {
matchers := []githubv4mock.Matcher{
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"},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
SetIssueFieldValue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
IssueFieldValues []struct {
TextValue struct {
Value string
} `graphql:"... on IssueFieldTextValue"`
SingleSelectValue struct {
Name string
} `graphql:"... on IssueFieldSingleSelectValue"`
DateValue struct {
Value string
} `graphql:"... on IssueFieldDateValue"`
NumberValue struct {
Value float64
} `graphql:"... on IssueFieldNumberValue"`
}
} `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",
},
},
}),
),
}
// Build a transport chain matching production: GraphQLFeaturesTransport
// wraps a header-capturing spy, which forwards to the mock's RoundTripper.
// This verifies the mutation request sets the update_issue_suggestions
// feature flag so the rationale/suggest input fields are accepted.
mockClient := githubv4mock.NewMockedHTTPClient(matchers...)
spy := &headerCaptureTransport{inner: mockClient.Transport}
httpClient := &http.Client{
Transport: &transportpkg.GraphQLFeaturesTransport{Transport: spy},
}
gqlClient := githubv4.NewClient(httpClient)
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)
require.False(t, result.IsError, getTextResult(t, result).Text)
// The last request captured is the mutation; the preceding issue ID
// query does not require the feature flag.
assert.Equal(t, "update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader))
})
}
// --- Reaction granular tool handler tests ---
func TestGranularAddIssueReaction(t *testing.T) {
mockReaction := &gogithub.Reaction{
ID: gogithub.Ptr(int64(12345)),
Content: gogithub.Ptr("+1"),
}
tests := []struct {
name string
mockedClient *http.Client
args map[string]any
expectErr bool
}{
{
name: "add reaction to issue successfully",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesReactionsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockReaction),
}),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(42),
"content": "+1",
},
expectErr: false,
},
{
name: "missing owner returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"repo": "repo",
"issue_number": float64(42),
"content": "+1",
},
expectErr: true,
},
{
name: "missing content returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(42),
},
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := GranularAddIssueReaction(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectErr {
assert.True(t, result.IsError)
} else {
assert.False(t, result.IsError)
textContent := getTextResult(t, result)
var response MinimalResponse
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
assert.Equal(t, "12345", response.ID)
assert.Equal(t, "https://api.github.com/repos/owner/repo/issues/42/reactions/12345", response.URL)
}
})
}
}
func TestGranularAddIssueCommentReaction(t *testing.T) {
mockReaction := &gogithub.Reaction{
ID: gogithub.Ptr(int64(67890)),
Content: gogithub.Ptr("heart"),
}
tests := []struct {
name string
mockedClient *http.Client
args map[string]any
expectErr bool
}{
{
name: "add reaction to issue comment successfully",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusCreated, mockReaction),
}),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"comment_id": float64(999),
"content": "heart",
},
expectErr: false,
},
{
name: "missing comment_id returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"content": "heart",
},
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := GranularAddIssueCommentReaction(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectErr {
assert.True(t, result.IsError)
} else {
assert.False(t, result.IsError)
textContent := getTextResult(t, result)
var response MinimalResponse
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
assert.Equal(t, "67890", response.ID)
assert.Equal(t, "https://api.github.com/repos/owner/repo/issues/comments/999/reactions/67890", response.URL)
}
})
}
}
func TestGranularAddPullRequestReviewCommentReaction(t *testing.T) {
mockReaction := &gogithub.Reaction{
ID: gogithub.Ptr(int64(54321)),
Content: gogithub.Ptr("rocket"),
}
tests := []struct {
name string
mockedClient *http.Client
args map[string]any
expectErr bool
}{
{
name: "add reaction to PR review comment successfully",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusCreated, mockReaction),
}),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"comment_id": float64(888),
"content": "rocket",
},
expectErr: false,
},
{
name: "missing repo returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"owner": "owner",
"comment_id": float64(888),
"content": "rocket",
},
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := GranularAddPullRequestReviewCommentReaction(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectErr {
assert.True(t, result.IsError)
} else {
assert.False(t, result.IsError)
textContent := getTextResult(t, result)
var response MinimalResponse
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
assert.Equal(t, "54321", response.ID)
assert.Equal(t, "https://api.github.com/repos/owner/repo/pulls/comments/888/reactions/54321", response.URL)
}
})
}
}