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
2 changes: 1 addition & 1 deletion docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ runtime behavior (such as output formatting) won't appear here.

- **update_issue_assignees** - Update Issue Assignees
- **Required OAuth Scopes**: `repo`
- `assignees`: GitHub usernames to assign to this issue (string[], required)
- `assignees`: GitHub usernames to assign to this issue. ([], required)
- `issue_number`: The issue number to update (number, required)
- `owner`: Repository owner (username or organization) (string, required)
- `repo`: Repository name (string, required)
Expand Down
41 changes: 38 additions & 3 deletions pkg/github/__toolsnaps__/update_issue_assignees.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,48 @@
"readOnlyHint": false,
"title": "Update Issue Assignees"
},
"description": "Update the assignees of an existing issue. This replaces the current assignees with the provided list.",
"description": "Update the assignees of an existing issue. This replaces the current assignees with the provided list. When setting values, include a confidence level (LOW, MEDIUM, or HIGH) reflecting how certain you are about the choice.",
"inputSchema": {
"properties": {
"assignees": {
"description": "GitHub usernames to assign to this issue",
"description": "GitHub usernames to assign to this issue.",
"items": {
"type": "string"
"oneOf": [
{
"description": "GitHub username",
"type": "string"
},
{
"properties": {
"confidence": {
"description": "How confident you are in this choice. Use 'HIGH' for clear signal or explicit user request, 'MEDIUM' for reasonable inference with some ambiguity, 'LOW' for best guess with limited signal.",
"enum": [
"LOW",
"MEDIUM",
"HIGH"
],
"type": "string"
},
"is_suggestion": {
"description": "If true, this assignee is sent to the API as a suggestion (suggest:true) rather than an applied assignee. Whether the assignee is applied or recorded as a proposal is determined by the API.",
"type": "boolean"
},
"login": {
"description": "GitHub username",
"type": "string"
},
"rationale": {
"description": "One concise sentence explaining what specifically about the issue led you to choose this assignee. State the concrete signal (e.g. 'Authored the file the crash originates in').",
"maxLength": 280,
"type": "string"
}
},
"required": [
"login"
],
"type": "object"
}
]
},
"type": "array"
},
Expand Down
161 changes: 161 additions & 0 deletions pkg/github/granular_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,167 @@ func TestGranularUpdateIssueAssignees(t *testing.T) {
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",
},
}
Comment thread
kelsey-myers marked this conversation as resolved.

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
Expand Down
Loading
Loading