2626 lines
77 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|