Reduce project item response size

Return compact project item content and field values from project item tools to avoid verbose issue and pull request payloads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
RossTarrant
2026-05-28 10:09:06 +01:00
committed by Sam Morrow
parent 51a7383fdd
commit 122951007a
3 changed files with 852 additions and 8 deletions
+592
View File
@@ -2,7 +2,9 @@ package github
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/google/go-github/v87/github"
@@ -207,6 +209,68 @@ type MinimalProject struct {
OwnerType string `json:"owner_type,omitempty"`
}
type MinimalProjectItem struct {
ID int64 `json:"id"`
NodeID string `json:"node_id,omitempty"`
ContentType string `json:"content_type,omitempty"`
Content *MinimalProjectItemContent `json:"content,omitempty"`
Fields []MinimalProjectItemFieldValue `json:"fields,omitempty"`
ArchivedAt string `json:"archived_at,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Creator string `json:"creator,omitempty"`
}
type MinimalProjectItemContent struct {
ID int64 `json:"id,omitempty"`
NodeID string `json:"node_id,omitempty"`
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
State string `json:"state,omitempty"`
StateReason string `json:"state_reason,omitempty"`
HTMLURL string `json:"html_url,omitempty"`
Repository string `json:"repository,omitempty"`
Author string `json:"author,omitempty"`
Assignees []string `json:"assignees,omitempty"`
Labels []string `json:"labels,omitempty"`
Milestone string `json:"milestone,omitempty"`
Comments int `json:"comments,omitempty"`
Draft bool `json:"draft,omitempty"`
Merged bool `json:"merged,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
ClosedAt string `json:"closed_at,omitempty"`
MergedAt string `json:"merged_at,omitempty"`
}
type MinimalProjectItemFieldValue struct {
ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
DataType string `json:"data_type,omitempty"`
Value any `json:"value,omitempty"`
}
type minimalProjectOptionValue struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Color string `json:"color,omitempty"`
}
type minimalProjectIterationValue struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
StartDate string `json:"start_date,omitempty"`
Duration int `json:"duration,omitempty"`
}
type minimalProjectPullRequestRef struct {
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
State string `json:"state,omitempty"`
HTMLURL string `json:"html_url,omitempty"`
Repository string `json:"repository,omitempty"`
}
// MinimalReactions is the trimmed output type for reaction summaries, dropping the API URL.
type MinimalReactions struct {
TotalCount int `json:"total_count"`
@@ -788,6 +852,534 @@ func convertToMinimalProject(fullProject *github.ProjectV2) *MinimalProject {
}
}
func convertToMinimalProjectItem(item *github.ProjectV2Item) MinimalProjectItem {
if item == nil {
return MinimalProjectItem{}
}
contentType := ""
if item.ContentType != nil {
contentType = string(*item.ContentType)
}
creator := ""
if item.Creator != nil {
creator = item.Creator.GetLogin()
}
return MinimalProjectItem{
ID: item.GetID(),
NodeID: item.GetNodeID(),
ContentType: contentType,
Content: convertToMinimalProjectItemContent(item.GetContent()),
Fields: convertToMinimalProjectItemFields(item.GetFields()),
ArchivedAt: formatProjectTimestamp(item.ArchivedAt),
CreatedAt: formatProjectTimestamp(item.CreatedAt),
UpdatedAt: formatProjectTimestamp(item.UpdatedAt),
Creator: creator,
}
}
func convertToMinimalProjectItemContent(content *github.ProjectV2ItemContent) *MinimalProjectItemContent {
if content == nil {
return nil
}
if issue := content.GetIssue(); issue != nil {
return convertIssueToMinimalProjectItemContent(issue)
}
if pr := content.GetPullRequest(); pr != nil {
return convertPullRequestToMinimalProjectItemContent(pr)
}
if draftIssue := content.GetDraftIssue(); draftIssue != nil {
return convertDraftIssueToMinimalProjectItemContent(draftIssue)
}
return nil
}
func convertIssueToMinimalProjectItemContent(issue *github.Issue) *MinimalProjectItemContent {
m := &MinimalProjectItemContent{
ID: issue.GetID(),
NodeID: issue.GetNodeID(),
Number: issue.GetNumber(),
Title: issue.GetTitle(),
State: issue.GetState(),
StateReason: issue.GetStateReason(),
HTMLURL: issue.GetHTMLURL(),
Repository: issueRepositoryFullName(issue),
Comments: issue.GetComments(),
Draft: issue.GetDraft(),
CreatedAt: formatProjectTimestamp(issue.CreatedAt),
UpdatedAt: formatProjectTimestamp(issue.UpdatedAt),
ClosedAt: formatProjectTimestamp(issue.ClosedAt),
}
if user := issue.GetUser(); user != nil {
m.Author = user.GetLogin()
}
for _, assignee := range issue.Assignees {
if assignee != nil {
m.Assignees = append(m.Assignees, assignee.GetLogin())
}
}
for _, label := range issue.Labels {
if label != nil {
m.Labels = append(m.Labels, label.GetName())
}
}
if milestone := issue.GetMilestone(); milestone != nil {
m.Milestone = milestone.GetTitle()
}
return m
}
func convertPullRequestToMinimalProjectItemContent(pr *github.PullRequest) *MinimalProjectItemContent {
m := &MinimalProjectItemContent{
ID: pr.GetID(),
NodeID: pr.GetNodeID(),
Number: pr.GetNumber(),
Title: pr.GetTitle(),
State: pr.GetState(),
HTMLURL: pr.GetHTMLURL(),
Repository: pullRequestRepositoryFullName(pr),
Comments: pr.GetComments(),
Draft: pr.GetDraft(),
Merged: pr.GetMerged(),
CreatedAt: formatProjectTimestamp(pr.CreatedAt),
UpdatedAt: formatProjectTimestamp(pr.UpdatedAt),
ClosedAt: formatProjectTimestamp(pr.ClosedAt),
MergedAt: formatProjectTimestamp(pr.MergedAt),
}
if user := pr.GetUser(); user != nil {
m.Author = user.GetLogin()
}
for _, assignee := range pr.Assignees {
if assignee != nil {
m.Assignees = append(m.Assignees, assignee.GetLogin())
}
}
for _, label := range pr.Labels {
if label != nil {
m.Labels = append(m.Labels, label.GetName())
}
}
if milestone := pr.GetMilestone(); milestone != nil {
m.Milestone = milestone.GetTitle()
}
return m
}
func convertDraftIssueToMinimalProjectItemContent(draftIssue *github.ProjectV2DraftIssue) *MinimalProjectItemContent {
m := &MinimalProjectItemContent{
ID: draftIssue.GetID(),
NodeID: draftIssue.GetNodeID(),
Title: draftIssue.GetTitle(),
CreatedAt: formatProjectTimestamp(draftIssue.CreatedAt),
UpdatedAt: formatProjectTimestamp(draftIssue.UpdatedAt),
}
if user := draftIssue.GetUser(); user != nil {
m.Author = user.GetLogin()
}
return m
}
func convertToMinimalProjectItemFields(fields []*github.ProjectV2ItemFieldValue) []MinimalProjectItemFieldValue {
minimalFields := make([]MinimalProjectItemFieldValue, 0, len(fields))
for _, field := range fields {
if field == nil {
continue
}
minimalFields = append(minimalFields, MinimalProjectItemFieldValue{
ID: field.GetID(),
Name: field.GetName(),
DataType: field.GetDataType(),
Value: minimalProjectFieldValue(field.GetValue()),
})
}
return minimalFields
}
func minimalProjectFieldValue(value any) any {
switch v := value.(type) {
case nil:
return nil
case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return v
case []string:
return v
case map[string]any:
return minimalProjectMapValue(v)
case []any:
return minimalProjectArrayValue(v)
case *github.User:
return v.GetLogin()
case *github.Label:
return v.GetName()
case *github.Repository:
return v.GetFullName()
case *github.Milestone:
return v.GetTitle()
case *github.PullRequest:
return minimalProjectPullRequestRefFromPullRequest(v)
case *github.ProjectV2FieldOption:
return minimalProjectOptionValue{
ID: v.GetID(),
Name: projectTextContentString(v.GetName()),
Color: v.GetColor(),
}
case *github.ProjectV2FieldIteration:
return minimalProjectIterationValue{
ID: v.GetID(),
Title: projectTextContentString(v.GetTitle()),
StartDate: v.GetStartDate(),
Duration: v.GetDuration(),
}
case []*github.User:
logins := make([]string, 0, len(v))
for _, user := range v {
if user != nil {
logins = append(logins, user.GetLogin())
}
}
return logins
case []*github.Label:
names := make([]string, 0, len(v))
for _, label := range v {
if label != nil {
names = append(names, label.GetName())
}
}
return names
case []*github.PullRequest:
refs := make([]minimalProjectPullRequestRef, 0, len(v))
for _, pr := range v {
if pr != nil {
refs = append(refs, minimalProjectPullRequestRefFromPullRequest(pr))
}
}
return refs
default:
return nil
}
}
func minimalProjectMapValue(value map[string]any) any {
if text := minimalProjectTextValue(value); text != "" {
return text
}
if repo := fullNameFromMap(value); repo != "" {
return repo
}
if login := stringFromMap(value, "login"); login != "" {
return login
}
if isPullRequestMap(value) {
return minimalProjectPullRequestRefFromMap(value)
}
if option, ok := minimalProjectOptionFromMap(value); ok {
return option
}
if iteration, ok := minimalProjectIterationFromMap(value); ok {
return iteration
}
if title := stringFromMap(value, "title"); title != "" {
return title
}
if name := stringFromMap(value, "name"); name != "" {
return name
}
compact := make(map[string]any)
for key, nestedValue := range value {
minimalValue := minimalProjectFieldValue(nestedValue)
if shouldKeepMinimalProjectValue(minimalValue) {
compact[key] = minimalValue
}
}
if len(compact) == 0 {
return nil
}
return compact
}
func minimalProjectArrayValue(values []any) any {
if refs, ok := minimalProjectPullRequestRefsFromArray(values); ok {
return refs
}
if strings, ok := minimalProjectStringsFromArray(values, "login"); ok {
return strings
}
if strings, ok := minimalProjectStringsFromArray(values, "name"); ok {
return strings
}
compact := make([]any, 0, len(values))
for _, value := range values {
minimalValue := minimalProjectFieldValue(value)
if shouldKeepMinimalProjectValue(minimalValue) {
compact = append(compact, minimalValue)
}
}
if len(compact) == 0 {
return nil
}
return compact
}
func minimalProjectTextValue(value map[string]any) string {
if raw := stringFromMap(value, "raw"); raw != "" {
return raw
}
if html := stringFromMap(value, "html"); html != "" {
return html
}
return stringFromMap(value, "text")
}
func minimalProjectOptionFromMap(value map[string]any) (minimalProjectOptionValue, bool) {
name := stringFromMap(value, "name")
color := stringFromMap(value, "color")
if name == "" && color == "" {
return minimalProjectOptionValue{}, false
}
return minimalProjectOptionValue{
ID: stringFromMap(value, "id"),
Name: name,
Color: color,
}, true
}
func minimalProjectIterationFromMap(value map[string]any) (minimalProjectIterationValue, bool) {
startDate := stringFromMap(value, "start_date")
duration := intFromAny(value["duration"])
if startDate == "" && duration == 0 {
return minimalProjectIterationValue{}, false
}
return minimalProjectIterationValue{
ID: stringFromMap(value, "id"),
Title: stringFromMap(value, "title"),
StartDate: startDate,
Duration: duration,
}, true
}
func minimalProjectPullRequestRefsFromArray(values []any) ([]minimalProjectPullRequestRef, bool) {
refs := make([]minimalProjectPullRequestRef, 0, len(values))
for _, value := range values {
switch pr := value.(type) {
case map[string]any:
if !isPullRequestMap(pr) {
return nil, false
}
refs = append(refs, minimalProjectPullRequestRefFromMap(pr))
case *github.PullRequest:
if pr == nil {
continue
}
refs = append(refs, minimalProjectPullRequestRefFromPullRequest(pr))
default:
return nil, false
}
}
return refs, len(refs) > 0
}
func minimalProjectStringsFromArray(values []any, key string) ([]string, bool) {
strings := make([]string, 0, len(values))
for _, value := range values {
switch v := value.(type) {
case map[string]any:
stringValue := stringFromMap(v, key)
if stringValue == "" {
return nil, false
}
strings = append(strings, stringValue)
case *github.User:
if key != "login" || v == nil {
return nil, false
}
strings = append(strings, v.GetLogin())
case *github.Label:
if key != "name" || v == nil {
return nil, false
}
strings = append(strings, v.GetName())
default:
return nil, false
}
}
return strings, len(strings) > 0
}
func minimalProjectPullRequestRefFromPullRequest(pr *github.PullRequest) minimalProjectPullRequestRef {
if pr == nil {
return minimalProjectPullRequestRef{}
}
return minimalProjectPullRequestRef{
Number: pr.GetNumber(),
Title: pr.GetTitle(),
State: pr.GetState(),
HTMLURL: pr.GetHTMLURL(),
Repository: pullRequestRepositoryFullName(pr),
}
}
func minimalProjectPullRequestRefFromMap(value map[string]any) minimalProjectPullRequestRef {
htmlURL := stringFromMap(value, "html_url")
repository := fullNameFromMapValue(value["repository"])
if repository == "" {
repository = branchRepositoryFullNameFromMap(value, "base")
}
if repository == "" {
repository = branchRepositoryFullNameFromMap(value, "head")
}
if repository == "" {
repository = repositoryFromHTMLURL(htmlURL)
}
return minimalProjectPullRequestRef{
Number: intFromAny(value["number"]),
Title: stringFromMap(value, "title"),
State: stringFromMap(value, "state"),
HTMLURL: htmlURL,
Repository: repository,
}
}
func isPullRequestMap(value map[string]any) bool {
return intFromAny(value["number"]) != 0 && (stringFromMap(value, "html_url") != "" || stringFromMap(value, "state") != "")
}
func branchRepositoryFullNameFromMap(value map[string]any, branchKey string) string {
branch, ok := value[branchKey].(map[string]any)
if !ok {
return ""
}
return fullNameFromMapValue(branch["repo"])
}
func shouldKeepMinimalProjectValue(value any) bool {
switch v := value.(type) {
case nil:
return false
case string:
return v != ""
case []any:
return len(v) > 0
case []string:
return len(v) > 0
case []minimalProjectPullRequestRef:
return len(v) > 0
case map[string]any:
return len(v) > 0
default:
return true
}
}
func issueRepositoryFullName(issue *github.Issue) string {
if repo := issue.GetRepository(); repo != nil {
return repo.GetFullName()
}
return repositoryFromHTMLURL(issue.GetHTMLURL())
}
func pullRequestRepositoryFullName(pr *github.PullRequest) string {
if base := pr.GetBase(); base != nil {
if repo := base.GetRepo(); repo != nil && repo.GetFullName() != "" {
return repo.GetFullName()
}
}
if head := pr.GetHead(); head != nil {
if repo := head.GetRepo(); repo != nil && repo.GetFullName() != "" {
return repo.GetFullName()
}
}
return repositoryFromHTMLURL(pr.GetHTMLURL())
}
func fullNameFromMapValue(value any) string {
repo, ok := value.(map[string]any)
if !ok {
return ""
}
return fullNameFromMap(repo)
}
func fullNameFromMap(value map[string]any) string {
return stringFromMap(value, "full_name")
}
func repositoryFromHTMLURL(rawURL string) string {
if rawURL == "" {
return ""
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return ""
}
parts := strings.Split(strings.Trim(parsedURL.Path, "/"), "/")
if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
return ""
}
return parts[0] + "/" + parts[1]
}
func projectTextContentString(content *github.ProjectV2TextContent) string {
if content == nil {
return ""
}
if raw := content.GetRaw(); raw != "" {
return raw
}
return content.GetHTML()
}
func formatProjectTimestamp(timestamp *github.Timestamp) string {
if timestamp == nil || timestamp.IsZero() {
return ""
}
return timestamp.Format(time.RFC3339)
}
func stringFromMap(value map[string]any, key string) string {
return stringFromAny(value[key])
}
func stringFromAny(value any) string {
switch v := value.(type) {
case string:
return v
case fmt.Stringer:
return v.String()
default:
return ""
}
}
func intFromAny(value any) int {
switch v := value.(type) {
case int:
return v
case float64:
return int(v)
case string:
i, err := strconv.Atoi(v)
if err != nil {
return 0
}
return i
default:
return 0
}
}
func convertToMinimalUser(user *github.User) *MinimalUser {
if user == nil {
return nil
+8 -3
View File
@@ -820,8 +820,13 @@ func listProjectItems(ctx context.Context, client *github.Client, args map[strin
}
defer func() { _ = resp.Body.Close() }()
minimalItems := make([]MinimalProjectItem, 0, len(projectItems))
for _, item := range projectItems {
minimalItems = append(minimalItems, convertToMinimalProjectItem(item))
}
response := map[string]any{
"items": projectItems,
"items": minimalItems,
"pageInfo": buildPageInfo(resp),
}
@@ -939,7 +944,7 @@ func getProjectItem(ctx context.Context, client *github.Client, owner, ownerType
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get project item", resp, body), nil, nil
}
r, err := json.Marshal(projectItem)
r, err := json.Marshal(convertToMinimalProjectItem(projectItem))
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
}
@@ -978,7 +983,7 @@ func updateProjectItem(ctx context.Context, client *github.Client, owner, ownerT
}
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectUpdateFailedError, resp, body), nil, nil
}
r, err := json.Marshal(updatedItem)
r, err := json.Marshal(convertToMinimalProjectItem(updatedItem))
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
}
+252 -5
View File
@@ -185,10 +185,152 @@ func Test_ProjectsList_ListProjectFields(t *testing.T) {
})
}
func verbosePullRequestProjectItemFixture() map[string]any {
return map[string]any{
"id": 1001,
"node_id": "PVTI_1",
"content_type": "PullRequest",
"item_url": "https://api.github.com/projectsV2/items/1001",
"project_url": "https://api.github.com/orgs/octo-org/projectsV2/1",
"creator": map[string]any{
"login": "creator",
"id": 999,
"followers_url": "https://api.github.com/users/creator/followers",
},
"content": map[string]any{
"id": 2002,
"node_id": "PR_1",
"number": 42,
"title": "Reduce project item output",
"body": "Long pull request body that should not be returned from project item tools.",
"state": "closed",
"html_url": "https://github.com/cli/cli/pull/42",
"url": "https://api.github.com/repos/cli/cli/pulls/42",
"diff_url": "https://github.com/cli/cli/pull/42.diff",
"patch_url": "https://github.com/cli/cli/pull/42.patch",
"draft": false,
"merged": true,
"created_at": "2026-05-07T18:41:21Z",
"updated_at": "2026-05-07T21:21:57Z",
"closed_at": "2026-05-07T21:21:55Z",
"merged_at": "2026-05-07T21:21:55Z",
"user": map[string]any{
"login": "octocat",
"id": 123,
"followers_url": "https://api.github.com/users/octocat/followers",
},
"assignees": []map[string]any{
{
"login": "hubot",
"events_url": "https://api.github.com/users/hubot/events{/privacy}",
},
},
"labels": []map[string]any{
{
"name": "bug",
"url": "https://api.github.com/repos/cli/cli/labels/bug",
},
},
"milestone": map[string]any{
"title": "v1.0",
"description": "Verbose milestone description",
},
"head": map[string]any{
"ref": "feature",
"repo": map[string]any{
"full_name": "fork/cli",
"archive_url": "https://api.github.com/repos/fork/cli/{archive_format}{/ref}",
},
},
"base": map[string]any{
"ref": "trunk",
"repo": map[string]any{
"full_name": "cli/cli",
"archive_url": "https://api.github.com/repos/cli/cli/{archive_format}{/ref}",
},
},
"_links": map[string]any{
"self": map[string]any{
"href": "https://api.github.com/repos/cli/cli/pulls/42",
},
},
"statuses_url": "https://api.github.com/repos/cli/cli/statuses/abc123",
},
"fields": []map[string]any{
{
"id": 301,
"name": "Status",
"data_type": "single_select",
"value": map[string]any{
"id": "opt1",
"name": "Done",
"color": "GREEN",
"description": "Verbose option description",
},
},
},
"created_at": "2026-05-28T07:39:37Z",
"updated_at": "2026-05-28T07:40:15Z",
}
}
func assertMinimalPullRequestProjectItem(t *testing.T, rawJSON string, item map[string]any) {
t.Helper()
assert.Equal(t, float64(1001), item["id"])
assert.Equal(t, "PVTI_1", item["node_id"])
assert.Equal(t, "PullRequest", item["content_type"])
assert.Equal(t, "creator", item["creator"])
assert.Equal(t, "2026-05-28T07:39:37Z", item["created_at"])
assert.Equal(t, "2026-05-28T07:40:15Z", item["updated_at"])
content, ok := item["content"].(map[string]any)
require.True(t, ok)
assert.Equal(t, float64(42), content["number"])
assert.Equal(t, "Reduce project item output", content["title"])
assert.Equal(t, "closed", content["state"])
assert.Equal(t, "https://github.com/cli/cli/pull/42", content["html_url"])
assert.Equal(t, "cli/cli", content["repository"])
assert.Equal(t, "octocat", content["author"])
assert.Equal(t, true, content["merged"])
assert.Equal(t, "2026-05-07T18:41:21Z", content["created_at"])
assert.Equal(t, "2026-05-07T21:21:57Z", content["updated_at"])
assert.Equal(t, "2026-05-07T21:21:55Z", content["closed_at"])
assert.Equal(t, "2026-05-07T21:21:55Z", content["merged_at"])
assert.Equal(t, []any{"hubot"}, content["assignees"])
assert.Equal(t, []any{"bug"}, content["labels"])
assert.Equal(t, "v1.0", content["milestone"])
fields, ok := item["fields"].([]any)
require.True(t, ok)
require.Len(t, fields, 1)
field, ok := fields[0].(map[string]any)
require.True(t, ok)
assert.Equal(t, float64(301), field["id"])
assert.Equal(t, "Status", field["name"])
assert.Equal(t, "single_select", field["data_type"])
value, ok := field["value"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "opt1", value["id"])
assert.Equal(t, "Done", value["name"])
assert.Equal(t, "GREEN", value["color"])
assert.NotContains(t, rawJSON, `"body"`)
assert.NotContains(t, rawJSON, `"archive_url"`)
assert.NotContains(t, rawJSON, `"followers_url"`)
assert.NotContains(t, rawJSON, `"events_url"`)
assert.NotContains(t, rawJSON, `"_links"`)
assert.NotContains(t, rawJSON, `"head"`)
assert.NotContains(t, rawJSON, `"base"`)
assert.NotContains(t, rawJSON, `"url":`)
assert.NotContains(t, rawJSON, `"statuses_url"`)
assert.NotContains(t, rawJSON, `"diff_url"`)
}
func Test_ProjectsList_ListProjectItems(t *testing.T) {
toolDef := ProjectsList(translations.NullTranslationHelper)
items := []map[string]any{{"id": 1001, "archived_at": nil, "content": map[string]any{"title": "Issue 1"}}}
items := []map[string]any{verbosePullRequestProjectItemFixture()}
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
@@ -218,6 +360,9 @@ func Test_ProjectsList_ListProjectItems(t *testing.T) {
itemsList, ok := response["items"].([]any)
require.True(t, ok)
assert.Equal(t, 1, len(itemsList))
item, ok := itemsList[0].(map[string]any)
require.True(t, ok)
assertMinimalPullRequestProjectItem(t, textContent.Text, item)
})
}
@@ -352,7 +497,7 @@ func Test_ProjectsGet_GetProjectField(t *testing.T) {
func Test_ProjectsGet_GetProjectItem(t *testing.T) {
toolDef := ProjectsGet(translations.NullTranslationHelper)
item := map[string]any{"id": 1001, "archived_at": nil, "content": map[string]any{"title": "Issue 1"}}
item := verbosePullRequestProjectItemFixture()
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
@@ -380,7 +525,7 @@ func Test_ProjectsGet_GetProjectItem(t *testing.T) {
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.NotNil(t, response["id"])
assertMinimalPullRequestProjectItem(t, textContent.Text, response)
})
t.Run("missing item_id", func(t *testing.T) {
@@ -703,7 +848,7 @@ func Test_ProjectsWrite_AddProjectItem(t *testing.T) {
func Test_ProjectsWrite_UpdateProjectItem(t *testing.T) {
toolDef := ProjectsWrite(translations.NullTranslationHelper)
updatedItem := map[string]any{"id": 1001, "archived_at": nil}
updatedItem := verbosePullRequestProjectItemFixture()
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
@@ -735,7 +880,7 @@ func Test_ProjectsWrite_UpdateProjectItem(t *testing.T) {
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.NotNil(t, response["id"])
assertMinimalPullRequestProjectItem(t, textContent.Text, response)
})
t.Run("missing updated_field", func(t *testing.T) {
@@ -814,6 +959,108 @@ func Test_ProjectsWrite_DeleteProjectItem(t *testing.T) {
})
}
func TestMinimalProjectFieldValue(t *testing.T) {
tests := []struct {
name string
value any
want any
}{
{
name: "select option",
value: map[string]any{
"id": "opt1",
"name": "Done",
"color": "GREEN",
"description": "verbose",
},
want: minimalProjectOptionValue{
ID: "opt1",
Name: "Done",
Color: "GREEN",
},
},
{
name: "iteration",
value: map[string]any{
"id": "iter1",
"title": "Sprint 1",
"start_date": "2026-05-01",
"duration": float64(14),
},
want: minimalProjectIterationValue{
ID: "iter1",
Title: "Sprint 1",
StartDate: "2026-05-01",
Duration: 14,
},
},
{
name: "assignees",
value: []any{
map[string]any{"login": "octocat", "followers_url": "https://api.github.com/users/octocat/followers"},
map[string]any{"login": "hubot", "followers_url": "https://api.github.com/users/hubot/followers"},
},
want: []string{"octocat", "hubot"},
},
{
name: "labels",
value: []any{
map[string]any{"name": "bug", "url": "https://api.github.com/repos/cli/cli/labels/bug"},
map[string]any{"name": "help wanted", "url": "https://api.github.com/repos/cli/cli/labels/help%20wanted"},
},
want: []string{"bug", "help wanted"},
},
{
name: "repository",
value: map[string]any{
"full_name": "cli/cli",
"archive_url": "https://api.github.com/repos/cli/cli/{archive_format}{/ref}",
},
want: "cli/cli",
},
{
name: "linked pull requests",
value: []any{
map[string]any{
"number": float64(42),
"title": "Reduce output",
"state": "open",
"html_url": "https://github.com/cli/cli/pull/42",
"base": map[string]any{
"repo": map[string]any{
"full_name": "cli/cli",
"archive_url": "https://api.github.com/repos/cli/cli/{archive_format}{/ref}",
},
},
},
},
want: []minimalProjectPullRequestRef{
{
Number: 42,
Title: "Reduce output",
State: "open",
HTMLURL: "https://github.com/cli/cli/pull/42",
Repository: "cli/cli",
},
},
},
{
name: "raw text content",
value: map[string]any{
"raw": "plain text",
"html": "<p>plain text</p>",
},
want: "plain text",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, minimalProjectFieldValue(tc.value))
})
}
}
func Test_ProjectsList_ListProjectStatusUpdates(t *testing.T) {
toolDef := ProjectsList(translations.NullTranslationHelper)