Repository: cli/cli
Pull request #11065 (2025-06-03 13:28:11)

Fix assignees being dropped from `gh pr edit`
Fixes #11064.

Adds tests for both `issue edit` and `pr edit` even though `pr edit` is the broken one. 


Patch:
diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go
index 5e1f19b18..becbfce47 100644
--- a/pkg/cmd/pr/edit/edit.go
+++ b/pkg/cmd/pr/edit/edit.go
@@ -228,6 +228,7 @@ func editRun(opts *EditOptions) error {
 	if pr.AssignedActorsUsed {
 		editable.Assignees.ActorAssignees = true
 		editable.Assignees.Default = pr.AssignedActors.DisplayNames()
+		editable.Assignees.DefaultLogins = pr.AssignedActors.Logins()
 	} else {
 		editable.Assignees.Default = pr.Assignees.Logins()
 	}


Test patch:
diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go
index 4840cbf7a..d14b2f462 100644
--- a/pkg/cmd/issue/edit/edit_test.go
+++ b/pkg/cmd/issue/edit/edit_test.go
@@ -631,10 +631,11 @@ func Test_editRun(t *testing.T) {
 				},
 				EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error {
 					// Checking that the display name is being used in the prompt.
-					require.Equal(t, eo.Assignees.Default, []string{"hubot", "MonaLisa (Mona Display Name)"})
+					require.Equal(t, []string{"hubot"}, eo.Assignees.Default)
+					require.Equal(t, []string{"hubot"}, eo.Assignees.DefaultLogins)
 
-					// Mocking a selection of only MonaLisa in the prompt.
-					eo.Assignees.Value = []string{"MonaLisa (Mona Display Name)"}
+					// Adding MonaLisa as PR assignee, should preserve hubot.
+					eo.Assignees.Value = []string{"hubot", "MonaLisa (Mona Display Name)"}
 					return nil
 				},
 				FetchOptions:    prShared.FetchOptions,
@@ -662,7 +663,7 @@ func Test_editRun(t *testing.T) {
 							// Checking that despite the display name being returned
 							// from the EditFieldsSurvey, the ID is still
 							// used in the mutation.
-							require.Contains(t, inputs["actorIds"], "MONAID")
+							require.Subset(t, inputs["actorIds"], []string{"MONAID", "HUBOTID"})
 						}),
 				)
 			},
@@ -809,15 +810,9 @@ func mockIsssueNumberGetWithAssignedActors(_ *testing.T, reg *httpmock.Registry,
 							"id": "HUBOTID",
 							"login": "hubot",
 							"__typename": "Bot"
-						},
-						{
-							"id": "MONAID",
-							"login": "MonaLisa",
-							"name": "Mona Display Name",
-							"__typename": "User"
 						}
 					],
-					"totalCount": 2
+					"totalCount": 1
 				}
 			} } } }`, number)),
 	)
diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go
index 0c40e3839..374625912 100644
--- a/pkg/cmd/pr/edit/edit_test.go
+++ b/pkg/cmd/pr/edit/edit_test.go
@@ -532,8 +532,31 @@ func Test_editRun(t *testing.T) {
 					URL:                "https://github.com/OWNER/REPO/pull/123",
 					AssignedActorsUsed: true,
 				}, ghrepo.New("OWNER", "REPO")),
-				Interactive:     true,
-				Surveyor:        testSurveyor{},
+				Interactive: true,
+				Surveyor: testSurveyor{
+					fieldsToEdit: func(e *shared.Editable) error {
+						e.Title.Edited = true
+						e.Body.Edited = true
+						e.Reviewers.Edited = true
+						e.Assignees.Edited = true
+						e.Labels.Edited = true
+						e.Projects.Edited = true
+						e.Milestone.Edited = true
+						return nil
+					},
+					editFields: func(e *shared.Editable, _ string) error {
+						e.Title.Value = "new title"
+						e.Body.Value = "new body"
+						e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"}
+						e.Assignees.Value = []string{"monalisa", "hubot"}
+						e.Labels.Value = []string{"feature", "TODO", "bug"}
+						e.Labels.Add = []string{"feature", "TODO", "bug"}
+						e.Labels.Remove = []string{"docs"}
+						e.Projects.Value = []string{"Cleanup", "CleanupV2"}
+						e.Milestone.Value = "GA"
+						return nil
+					},
+				},
 				Fetcher:         testFetcher{},
 				EditorRetriever: testEditorRetriever{},
 			},
@@ -556,8 +579,29 @@ func Test_editRun(t *testing.T) {
 					URL:                "https://github.com/OWNER/REPO/pull/123",
 					AssignedActorsUsed: true,
 				}, ghrepo.New("OWNER", "REPO")),
-				Interactive:     true,
-				Surveyor:        testSurveyor{skipReviewers: true},
+				Interactive: true,
+				Surveyor: testSurveyor{
+					fieldsToEdit: func(e *shared.Editable) error {
+						e.Title.Edited = true
+						e.Body.Edited = true
+						e.Assignees.Edited = true
+						e.Labels.Edited = true
+						e.Projects.Edited = true
+						e.Milestone.Edited = true
+						return nil
+					},
+					editFields: func(e *shared.Editable, _ string) error {
+						e.Title.Value = "new title"
+						e.Body.Value = "new body"
+						e.Assignees.Value = []string{"monalisa", "hubot"}
+						e.Labels.Value = []string{"feature", "TODO", "bug"}
+						e.Labels.Add = []string{"feature", "TODO", "bug"}
+						e.Labels.Remove = []string{"docs"}
+						e.Projects.Value = []string{"Cleanup", "CleanupV2"}
+						e.Milestone.Value = "GA"
+						return nil
+					},
+				},
 				Fetcher:         testFetcher{},
 				EditorRetriever: testEditorRetriever{},
 			},
@@ -579,8 +623,31 @@ func Test_editRun(t *testing.T) {
 					URL:                "https://github.com/OWNER/REPO/pull/123",
 					AssignedActorsUsed: true,
 				}, ghrepo.New("OWNER", "REPO")),
-				Interactive:     true,
-				Surveyor:        testSurveyor{removeAllReviewers: true},
+				Interactive: true,
+				Surveyor: testSurveyor{
+					fieldsToEdit: func(e *shared.Editable) error {
+						e.Title.Edited = true
+						e.Body.Edited = true
+						e.Reviewers.Edited = true
+						e.Assignees.Edited = true
+						e.Labels.Edited = true
+						e.Projects.Edited = true
+						e.Milestone.Edited = true
+						return nil
+					},
+					editFields: func(e *shared.Editable, _ string) error {
+						e.Title.Value = "new title"
+						e.Body.Value = "new body"
+						e.Reviewers.Remove = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external", "dependabot"}
+						e.Assignees.Value = []string{"monalisa", "hubot"}
+						e.Labels.Value = []string{"feature", "TODO", "bug"}
+						e.Labels.Add = []string{"feature", "TODO", "bug"}
+						e.Labels.Remove = []string{"docs"}
+						e.Projects.Value = []string{"Cleanup", "CleanupV2"}
+						e.Milestone.Value = "GA"
+						return nil
+					},
+				},
 				Fetcher:         testFetcher{},
 				EditorRetriever: testEditorRetriever{},
 			},
@@ -594,6 +661,71 @@ func Test_editRun(t *testing.T) {
 			},
 			stdout: "https://github.com/OWNER/REPO/pull/123\n",
 		},
+		{
+			name: "interactive prompts with actor assignee display names when actors available",
+			input: &EditOptions{
+				Detector:    &fd.EnabledDetectorMock{},
+				SelectorArg: "123",
+				Finder: shared.NewMockFinder("123", &api.PullRequest{
+					URL:                "https://github.com/OWNER/REPO/pull/123",
+					AssignedActorsUsed: true,
+					AssignedActors: api.AssignedActors{
+						Nodes: []api.Actor{
+							{
+								ID:       "HUBOTID",
+								Login:    "hubot",
+								TypeName: "Bot",
+							},
+						},
+						TotalCount: 1,
+					},
+				}, ghrepo.New("OWNER", "REPO")),
+				Interactive: true,
+				Surveyor: testSurveyor{
+					fieldsToEdit: func(e *shared.Editable) error {
+						e.Assignees.Edited = true
+						return nil
+					},
+					editFields: func(e *shared.Editable, _ string) error {
+						// Checking that the display name is being used in the prompt.
+						require.Equal(t, []string{"hubot"}, e.Assignees.Default)
+						require.Equal(t, []string{"hubot"}, e.Assignees.DefaultLogins)
+
+						// Adding MonaLisa as PR assignee, should preserve hubot.
+						e.Assignees.Value = []string{"hubot", "MonaLisa (Mona Display Name)"}
+						return nil
+					},
+				},
+				Fetcher:         testFetcher{},
+				EditorRetriever: testEditorRetriever{},
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query RepositoryAssignableActors\b`),
+					httpmock.StringResponse(`
+					{ "data": { "repository": { "suggestedActors": {
+						"nodes": [
+							{ "login": "hubot", "id": "HUBOTID", "__typename": "Bot" },
+							{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" }
+						],
+						"pageInfo": { "hasNextPage": false }
+					} } } }
+					`))
+				mockPullRequestUpdate(reg)
+				reg.Register(
+					httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
+					httpmock.GraphQLMutation(`
+					{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }`,
+						func(inputs map[string]interface{}) {
+							// Checking that despite the display name being returned
+							// from the EditFieldsSurvey, the ID is still
+							// used in the mutation.
+							require.Subset(t, inputs["actorIds"], []string{"MONAID", "HUBOTID"})
+						}),
+				)
+			},
+			stdout: "https://github.com/OWNER/REPO/pull/123\n",
+		},
 		{
 			name: "Legacy assignee users are fetched and updated on unsupported GitHub Hosts",
 			input: &EditOptions{
@@ -666,7 +798,7 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) {
 			{ "data": { "repository": { "suggestedActors": {
 				"nodes": [
 					{ "login": "hubot", "id": "HUBOTID", "__typename": "Bot" },
-					{ "login": "MonaLisa", "id": "MONAID", "__typename": "User" }
+					{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" }
 				],
 				"pageInfo": { "hasNextPage": false }
 			} } } }
@@ -813,48 +945,26 @@ func mockProjectV2ItemUpdate(reg *httpmock.Registry) {
 }
 
 type testFetcher struct{}
-type testSurveyor struct {
-	skipReviewers      bool
-	removeAllReviewers bool
-}
-type testEditorRetriever struct{}
 
 func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error {
 	return shared.FetchOptions(client, repo, opts)
 }
 
+type testSurveyor struct {
+	fieldsToEdit func(e *shared.Editable) error
+	editFields   func(e *shared.Editable, editorCmd string) error
+}
+
 func (s testSurveyor) FieldsToEdit(e *shared.Editable) error {
-	e.Title.Edited = true
-	e.Body.Edited = true
-	if !s.skipReviewers {
-		e.Reviewers.Edited = true
-	}
-	e.Assignees.Edited = true
-	e.Labels.Edited = true
-	e.Projects.Edited = true
-	e.Milestone.Edited = true
-	return nil
+	return s.fieldsToEdit(e)
 }
 
-func (s testSurveyor) EditFields(e *shared.Editable, _ string) error {
-	e.Title.Value = "new title"
-	e.Body.Value = "new body"
-	if !s.skipReviewers {
-		if s.removeAllReviewers {
-			e.Reviewers.Remove = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external", "dependabot"}
-		} else {
-			e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"}
-		}
-	}
-	e.Assignees.Value = []string{"monalisa", "hubot"}
-	e.Labels.Value = []string{"feature", "TODO", "bug"}
-	e.Labels.Add = []string{"feature", "TODO", "bug"}
-	e.Labels.Remove = []string{"docs"}
-	e.Projects.Value = []string{"Cleanup", "CleanupV2"}
-	e.Milestone.Value = "GA"
-	return nil
+func (s testSurveyor) EditFields(e *shared.Editable, editorCmd string) error {
+	return s.editFields(e, editorCmd)
 }
 
+type testEditorRetriever struct{}
+
 func (t testEditorRetriever) Retrieve() (string, error) {
 	return "vim", nil
 }