Skip to content

Commit e3d7f90

Browse files
Merge branch 'main' into sammorrowdrums/gate-issue-write
2 parents 0ee3ef6 + d661abf commit e3d7f90

6 files changed

Lines changed: 251 additions & 68 deletions

File tree

docs/feature-flags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ runtime behavior (such as output formatting) won't appear here.
198198

199199
- **update_issue_type** - Update Issue Type
200200
- **Required OAuth Scopes**: `repo`
201-
- `is_suggestion`: If true, propose the issue type change instead of applying it. Defaults to false, which applies the change to the issue. (boolean, optional)
201+
- `is_suggestion`: If true, this issue type change is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the type is applied or recorded as a proposal is determined by the API. (boolean, optional)
202202
- `issue_number`: The issue number to update (number, required)
203203
- `issue_type`: The issue type to set (string, required)
204204
- `owner`: Repository owner (username or organization) (string, required)

pkg/github/__toolsnaps__/set_issue_fields.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"description": "The GraphQL node ID of the issue field",
2424
"type": "string"
2525
},
26+
"is_suggestion": {
27+
"description": "If true, this field value is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the value is applied or recorded as a proposal is determined by the API.",
28+
"type": "boolean"
29+
},
2630
"number_value": {
2731
"description": "The value to set for a number field",
2832
"type": "number"

pkg/github/__toolsnaps__/update_issue_labels.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
},
2323
{
2424
"properties": {
25+
"is_suggestion": {
26+
"description": "If true, this label is sent to the API as a suggestion (suggest:true) rather than an applied label. Whether the label is applied or recorded as a proposal is determined by the API.",
27+
"type": "boolean"
28+
},
2529
"name": {
2630
"description": "Label name",
2731
"type": "string"

pkg/github/__toolsnaps__/update_issue_type.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"inputSchema": {
99
"properties": {
1010
"is_suggestion": {
11-
"description": "If true, propose the issue type change instead of applying it. Defaults to false, which applies the change to the issue.",
11+
"description": "If true, this issue type change is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the type is applied or recorded as a proposal is determined by the API.",
1212
"type": "boolean"
1313
},
1414
"issue_number": {

pkg/github/granular_tools_test.go

Lines changed: 200 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package github
22

33
import (
44
"context"
5-
"encoding/json"
65
"net/http"
76
"strings"
87
"testing"
@@ -336,6 +335,84 @@ func TestGranularUpdateIssueLabels(t *testing.T) {
336335
}
337336
}
338337

338+
func TestGranularUpdateIssueLabelsSuggest(t *testing.T) {
339+
tests := []struct {
340+
name string
341+
requestArgs map[string]any
342+
expectedReq map[string]any
343+
}{
344+
{
345+
name: "single label suggested without rationale",
346+
requestArgs: map[string]any{
347+
"owner": "owner",
348+
"repo": "repo",
349+
"issue_number": float64(1),
350+
"labels": []any{
351+
map[string]any{"name": "bug", "is_suggestion": true},
352+
},
353+
},
354+
expectedReq: map[string]any{
355+
"labels": []any{
356+
map[string]any{"name": "bug", "suggest": true},
357+
},
358+
},
359+
},
360+
{
361+
name: "suggested label with rationale",
362+
requestArgs: map[string]any{
363+
"owner": "owner",
364+
"repo": "repo",
365+
"issue_number": float64(1),
366+
"labels": []any{
367+
map[string]any{"name": "frontend", "rationale": "Mentions the UI button", "is_suggestion": true},
368+
},
369+
},
370+
expectedReq: map[string]any{
371+
"labels": []any{
372+
map[string]any{"name": "frontend", "rationale": "Mentions the UI button", "suggest": true},
373+
},
374+
},
375+
},
376+
{
377+
name: "mix of plain, applied-with-rationale, and suggested labels",
378+
requestArgs: map[string]any{
379+
"owner": "owner",
380+
"repo": "repo",
381+
"issue_number": float64(1),
382+
"labels": []any{
383+
"triage",
384+
map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
385+
map[string]any{"name": "needs-design", "is_suggestion": true},
386+
},
387+
},
388+
expectedReq: map[string]any{
389+
"labels": []any{
390+
"triage",
391+
map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
392+
map[string]any{"name": "needs-design", "suggest": true},
393+
},
394+
},
395+
},
396+
}
397+
398+
for _, tc := range tests {
399+
t.Run(tc.name, func(t *testing.T) {
400+
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
401+
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
402+
andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
403+
}))
404+
deps := BaseDeps{Client: client}
405+
serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
406+
handler := serverTool.Handler(deps)
407+
408+
request := createMCPRequest(tc.requestArgs)
409+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
410+
require.NoError(t, err)
411+
assert.False(t, result.IsError)
412+
})
413+
}
414+
}
415+
339416
func TestGranularUpdateIssueLabelsInvalidRationale(t *testing.T) {
340417
tests := []struct {
341418
name string
@@ -463,62 +540,58 @@ func TestGranularUpdateIssueTypeSuggest(t *testing.T) {
463540
tests := []struct {
464541
name string
465542
requestArgs map[string]any
466-
expected map[string]any
543+
expectedReq map[string]any
467544
}{
468545
{
469546
name: "suggest without rationale",
470547
requestArgs: map[string]any{
471-
"owner": "owner",
472-
"repo": "repo",
473-
"issue_number": float64(1),
474-
"issue_type": "bug",
475-
"suggest": true,
548+
"owner": "owner",
549+
"repo": "repo",
550+
"issue_number": float64(1),
551+
"issue_type": "bug",
552+
"is_suggestion": true,
476553
},
477-
expected: map[string]any{
478-
"owner": "owner",
479-
"repo": "repo",
480-
"issue_number": float64(1),
481-
"issue_type": "bug",
482-
"suggested": true,
554+
expectedReq: map[string]any{
555+
"type": map[string]any{
556+
"value": "bug",
557+
"suggest": true,
558+
},
483559
},
484560
},
485561
{
486562
name: "suggest with rationale",
487563
requestArgs: map[string]any{
488-
"owner": "owner",
489-
"repo": "repo",
490-
"issue_number": float64(1),
491-
"issue_type": "feature",
492-
"rationale": " Asks for dark mode support ",
493-
"suggest": true,
564+
"owner": "owner",
565+
"repo": "repo",
566+
"issue_number": float64(1),
567+
"issue_type": "feature",
568+
"rationale": " Asks for dark mode support ",
569+
"is_suggestion": true,
494570
},
495-
expected: map[string]any{
496-
"owner": "owner",
497-
"repo": "repo",
498-
"issue_number": float64(1),
499-
"issue_type": "feature",
500-
"rationale": "Asks for dark mode support",
501-
"suggested": true,
571+
expectedReq: map[string]any{
572+
"type": map[string]any{
573+
"value": "feature",
574+
"rationale": "Asks for dark mode support",
575+
"suggest": true,
576+
},
502577
},
503578
},
504579
}
505580

506581
for _, tc := range tests {
507582
t.Run(tc.name, func(t *testing.T) {
508-
// No HTTP handler registered: any API call would fail the test.
509-
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
583+
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
584+
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
585+
andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
586+
}))
587+
deps := BaseDeps{Client: client}
510588
serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
511589
handler := serverTool.Handler(deps)
512590

513591
request := createMCPRequest(tc.requestArgs)
514592
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
515593
require.NoError(t, err)
516-
require.False(t, result.IsError)
517-
518-
textContent := getTextResult(t, result)
519-
var got map[string]any
520-
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &got))
521-
assert.Equal(t, tc.expected, got)
594+
assert.False(t, result.IsError)
522595
})
523596
}
524597
}
@@ -1312,4 +1385,97 @@ func TestGranularSetIssueFields(t *testing.T) {
13121385
textContent := getTextResult(t, result)
13131386
assert.Contains(t, textContent.Text, "field rationale must be 280 characters or less")
13141387
})
1388+
1389+
t.Run("successful set with suggest flag", func(t *testing.T) {
1390+
suggestTrue := githubv4.Boolean(true)
1391+
matchers := []githubv4mock.Matcher{
1392+
githubv4mock.NewQueryMatcher(
1393+
struct {
1394+
Repository struct {
1395+
Issue struct {
1396+
ID githubv4.ID
1397+
} `graphql:"issue(number: $issueNumber)"`
1398+
} `graphql:"repository(owner: $owner, name: $repo)"`
1399+
}{},
1400+
map[string]any{
1401+
"owner": githubv4.String("owner"),
1402+
"repo": githubv4.String("repo"),
1403+
"issueNumber": githubv4.Int(5),
1404+
},
1405+
githubv4mock.DataResponse(map[string]any{
1406+
"repository": map[string]any{
1407+
"issue": map[string]any{"id": "ISSUE_123"},
1408+
},
1409+
}),
1410+
),
1411+
githubv4mock.NewMutationMatcher(
1412+
struct {
1413+
SetIssueFieldValue struct {
1414+
Issue struct {
1415+
ID githubv4.ID
1416+
Number githubv4.Int
1417+
URL githubv4.String
1418+
}
1419+
IssueFieldValues []struct {
1420+
TextValue struct {
1421+
Value string
1422+
} `graphql:"... on IssueFieldTextValue"`
1423+
SingleSelectValue struct {
1424+
Name string
1425+
} `graphql:"... on IssueFieldSingleSelectValue"`
1426+
DateValue struct {
1427+
Value string
1428+
} `graphql:"... on IssueFieldDateValue"`
1429+
NumberValue struct {
1430+
Value float64
1431+
} `graphql:"... on IssueFieldNumberValue"`
1432+
}
1433+
} `graphql:"setIssueFieldValue(input: $input)"`
1434+
}{},
1435+
SetIssueFieldValueInput{
1436+
IssueID: githubv4.ID("ISSUE_123"),
1437+
IssueFields: []IssueFieldCreateOrUpdateInput{
1438+
{
1439+
FieldID: githubv4.ID("FIELD_1"),
1440+
TextValue: githubv4.NewString(githubv4.String("hello")),
1441+
Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")),
1442+
Suggest: &suggestTrue,
1443+
},
1444+
},
1445+
},
1446+
nil,
1447+
githubv4mock.DataResponse(map[string]any{
1448+
"setIssueFieldValue": map[string]any{
1449+
"issue": map[string]any{
1450+
"id": "ISSUE_123",
1451+
"number": 5,
1452+
"url": "https://github.com/owner/repo/issues/5",
1453+
},
1454+
},
1455+
}),
1456+
),
1457+
}
1458+
1459+
gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
1460+
deps := BaseDeps{GQLClient: gqlClient}
1461+
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
1462+
handler := serverTool.Handler(deps)
1463+
1464+
request := createMCPRequest(map[string]any{
1465+
"owner": "owner",
1466+
"repo": "repo",
1467+
"issue_number": float64(5),
1468+
"fields": []any{
1469+
map[string]any{
1470+
"field_id": "FIELD_1",
1471+
"text_value": "hello",
1472+
"rationale": "Reflects the reported severity",
1473+
"is_suggestion": true,
1474+
},
1475+
},
1476+
})
1477+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1478+
require.NoError(t, err)
1479+
assert.False(t, result.IsError)
1480+
})
13151481
}

0 commit comments

Comments
 (0)