Skip to content
Open
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
1 change: 1 addition & 0 deletions pkg/github/__toolsnaps__/list_issues.snap
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"state",
"user",
"labels",
"assignees",
"comments",
"created_at",
"updated_at",
Expand Down
54 changes: 53 additions & 1 deletion pkg/github/fields_filtering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func Test_SearchIssues_FieldsTelemetry(t *testing.T) {
// getIssueQueryType; see Test_ListIssues for the canonical copies.
const listIssuesFieldsFieldValuesSelection = "issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}}"

const listIssuesFieldsQuery = "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount}," + listIssuesFieldsFieldValuesSelection + "},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount},isPrivate}}"
const listIssuesFieldsQuery = "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},assignees(first: 100){nodes{login}},comments{totalCount}," + listIssuesFieldsFieldValuesSelection + "},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount},isPrivate}}"

func listIssuesFieldsMockClient() *http.Client {
vars := map[string]any{
Expand All @@ -301,6 +301,7 @@ func listIssuesFieldsMockClient() *http.Client {
"updatedAt": "2023-01-01T00:00:00Z",
"author": map[string]any{"login": "user1"},
"labels": map[string]any{"nodes": []map[string]any{}},
"assignees": map[string]any{"nodes": []map[string]any{{"login": "octocat"}}},
"comments": map[string]any{"totalCount": 1},
"issueFieldValues": map[string]any{"nodes": []map[string]any{}},
},
Expand Down Expand Up @@ -353,6 +354,57 @@ func Test_ListIssues_FieldFiltering(t *testing.T) {
assert.NotContains(t, textContent.Text, "\"body\"")
}

// Test_ListIssues_AssigneesField covers the assignees field end to end: it is
// selectable via fields, it is dropped when not requested, and it is always
// present in an unfiltered response so that "unassigned" reads as [] rather
// than an absent key.
func Test_ListIssues_AssigneesField(t *testing.T) {
serverTool := ListIssues(translations.NullTranslationHelper)

callWithFields := func(t *testing.T, fields []any) string {
t.Helper()
deps := BaseDeps{GQLClient: githubv4.NewClient(listIssuesFieldsMockClient())}
handler := serverTool.Handler(deps)

args := map[string]any{"owner": "owner", "repo": "repo"}
if fields != nil {
args["fields"] = fields
}
request := createMCPRequest(args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)
return getTextResult(t, result).Text
}

t.Run("selectable via fields", func(t *testing.T) {
var returned struct {
Issues []map[string]any `json:"issues"`
}
require.NoError(t, json.Unmarshal([]byte(callWithFields(t, []any{"number", "assignees"})), &returned))
require.Len(t, returned.Issues, 1)
require.Len(t, returned.Issues[0], 2, "only the two requested fields should be present")
assert.Equal(t, []any{"octocat"}, returned.Issues[0]["assignees"])
})

t.Run("omitted when not requested", func(t *testing.T) {
text := callWithFields(t, []any{"number", "title"})
assert.NotContains(t, text, "\"assignees\"")
})

t.Run("unassigned issues serialize as an empty array", func(t *testing.T) {
// The mock returns one assigned issue, so drive the empty case through
// the conversion directly: no assignees node must still yield [], never
// null and never an absent key.
issue := fragmentToMinimalIssue(IssueFragment{})
require.NotNil(t, issue.Assignees)

encoded, err := json.Marshal(issue)
require.NoError(t, err)
assert.Contains(t, string(encoded), "\"assignees\":[]")
})
}

func Test_ListIssues_FieldsTelemetry(t *testing.T) {
serverTool := ListIssues(translations.NullTranslationHelper)

Expand Down
6 changes: 6 additions & 0 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,12 @@ type IssueFragment struct {
Description githubv4.String
}
} `graphql:"labels(first: 100)"`
// GitHub caps issue assignees at 10, so first: 100 cannot truncate.
Assignees struct {
Nodes []struct {
Login githubv4.String
}
} `graphql:"assignees(first: 100)"`
Comments struct {
TotalCount githubv4.Int
} `graphql:"comments"`
Expand Down
Loading