Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions pkg/github/__toolsnaps__/set_issue_fields.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"annotations": {
"destructiveHint": false,
"openWorldHint": true,
"title": "Set Issue Fields"
},
"description": "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue.",
"inputSchema": {
"properties": {
"fields": {
"description": "Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, or 'single_select_option_id' (the GraphQL node ID of the option) for single select fields. Set 'delete' to true to remove a field value.",
"items": {
"properties": {
"date_value": {
"description": "The value to set for a date field (ISO 8601 date string)",
"type": "string"
},
"delete": {
"description": "Set to true to delete this field value",
"type": "boolean"
},
"field_id": {
"description": "The GraphQL node ID of the issue field",
"type": "string"
},
"number_value": {
"description": "The value to set for a number field",
"type": "number"
},
"single_select_option_id": {
"description": "The GraphQL node ID of the option to set for a single select field",
"type": "string"
},
"text_value": {
"description": "The value to set for a text field",
"type": "string"
}
},
"required": [
"field_id"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"issue_number": {
"description": "The issue number to update",
"minimum": 1,
"type": "number"
},
"owner": {
"description": "Repository owner (username or organization)",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo",
"issue_number",
"fields"
],
"type": "object"
},
"name": "set_issue_fields"
}
172 changes: 172 additions & 0 deletions pkg/github/granular_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestGranularToolSnaps(t *testing.T) {
GranularAddSubIssue,
GranularRemoveSubIssue,
GranularReprioritizeSubIssue,
GranularSetIssueFields,
GranularUpdatePullRequestTitle,
GranularUpdatePullRequestBody,
GranularUpdatePullRequestState,
Expand Down Expand Up @@ -81,6 +82,7 @@ func TestIssuesGranularToolset(t *testing.T) {
"add_sub_issue",
"remove_sub_issue",
"reprioritize_sub_issue",
"set_issue_fields",
}
for _, name := range expected {
assert.Contains(t, toolNames, name)
Expand Down Expand Up @@ -774,3 +776,173 @@ func TestGranularUnresolveReviewThread(t *testing.T) {
require.NoError(t, err)
assert.False(t, result.IsError)
}

func TestGranularSetIssueFields(t *testing.T) {
t.Run("successful set with text value", func(t *testing.T) {
matchers := []githubv4mock.Matcher{
// Mock the issue ID query
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Issue struct {
ID githubv4.ID
} `graphql:"issue(number: $issueNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"issueNumber": githubv4.Int(5),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"issue": map[string]any{"id": "ISSUE_123"},
},
}),
),
// Mock the setIssueFieldValue mutation
githubv4mock.NewMutationMatcher(
struct {
SetIssueFieldValue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
IssueFieldValues []struct {
Field struct {
Name string
} `graphql:"... on IssueFieldDateValue"`
}
} `graphql:"setIssueFieldValue(input: $input)"`
}{},
SetIssueFieldValueInput{
IssueID: githubv4.ID("ISSUE_123"),
IssueFields: []IssueFieldCreateOrUpdateInput{
{
FieldID: githubv4.ID("FIELD_1"),
TextValue: githubv4.NewString(githubv4.String("hello")),
},
},
},
nil,
githubv4mock.DataResponse(map[string]any{
"setIssueFieldValue": map[string]any{
"issue": map[string]any{
"id": "ISSUE_123",
"number": 5,
"url": "https://github.com/owner/repo/issues/5",
},
},
}),
),
}

gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(5),
"fields": []any{
map[string]any{"field_id": "FIELD_1", "text_value": "hello"},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
Comment thread
mattdholloway marked this conversation as resolved.

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")
})
}
Loading
Loading