@@ -243,9 +243,10 @@ func Test_optionalIssueWriteFields_MultiSelect(t *testing.T) {
243243 },
244244 })
245245 require .Error (t , err )
246- // An empty slice is treated the same as "neither value/option provided" — the
247- // caller has to pick something. delete:true is the right way to clear a field.
248- assert .Contains (t , err .Error (), "must specify one of" )
246+ // An empty slice is a common "clear the field" guess; nudge callers to delete:true
247+ // so the GraphQL deletion mutation runs instead of an unintentional no-op.
248+ assert .Contains (t , err .Error (), "empty field_option_names" )
249+ assert .Contains (t , err .Error (), "delete: true" )
249250 })
250251
251252 t .Run ("rejects field_option_names + value combo" , func (t * testing.T ) {
@@ -528,3 +529,144 @@ func Test_UpdateIssue_DeleteFieldValueRunsGraphQLMutation(t *testing.T) {
528529}
529530
530531// (intentionally no further helpers)
532+
533+ // Test_UpdateIssue_DeleteAndSetFieldsInSameCall verifies that a single UpdateIssue
534+ // can delete one issue field while setting another in the same request: the REST PATCH
535+ // must carry only the set operation (no null clearing for the deleted field), and the
536+ // deleteIssueFieldValue GraphQL mutation must fire for the deletion.
537+ func Test_UpdateIssue_DeleteAndSetFieldsInSameCall (t * testing.T ) {
538+ t .Parallel ()
539+
540+ mockIssue := & gogithub.Issue {
541+ Number : gogithub .Ptr (42 ),
542+ Title : gogithub .Ptr ("Test issue" ),
543+ State : gogithub .Ptr ("open" ),
544+ HTMLURL : gogithub .Ptr ("https://github.com/owner/repo/issues/42" ),
545+ }
546+
547+ // Verify the PATCH body carries only the kept set operation. Crucially, it must
548+ // NOT include the deleted Components field (omitting != sending null), and it
549+ // must NOT double-clear the deleted field — the GraphQL mutation owns that path.
550+ restClient := mustNewGHClient (t , MockHTTPClientWithHandlers (map [string ]http.HandlerFunc {
551+ PatchReposIssuesByOwnerByRepoByIssueNumber : expectRequestBody (t , map [string ]any {
552+ "issue_field_values" : []any {
553+ map [string ]any {"field_id" : float64 (202 ), "value" : "P2" },
554+ },
555+ }).andThen (
556+ mockResponse (t , http .StatusOK , mockIssue ),
557+ ),
558+ }))
559+
560+ // Existing issue carries both fields. After merge + deletion filter, only the
561+ // Priority "set" entry should remain in the REST payload.
562+ existingFieldsResponse := githubv4mock .DataResponse (map [string ]any {
563+ "repository" : map [string ]any {
564+ "issue" : map [string ]any {
565+ "issueFieldValues" : map [string ]any {
566+ "nodes" : []any {
567+ map [string ]any {
568+ "__typename" : "IssueFieldMultiSelectValue" ,
569+ "field" : map [string ]any {
570+ "fullDatabaseId" : "101" ,
571+ "name" : "Components" ,
572+ },
573+ "options" : []any {
574+ map [string ]any {"name" : "Auth" },
575+ map [string ]any {"name" : "Billing" },
576+ },
577+ },
578+ map [string ]any {
579+ "__typename" : "IssueFieldSingleSelectValue" ,
580+ "field" : map [string ]any {
581+ "fullDatabaseId" : "202" ,
582+ "name" : "Priority" ,
583+ },
584+ "value" : "P1" ,
585+ },
586+ },
587+ },
588+ },
589+ },
590+ })
591+
592+ issueIDResponse := githubv4mock .DataResponse (map [string ]any {
593+ "repository" : map [string ]any {
594+ "issue" : map [string ]any {"id" : "I_node_42" },
595+ },
596+ })
597+
598+ // The matcher fails the test if the mutation doesn't fire with the right
599+ // (issueId, fieldId) pair — proves the delete side of the combined update ran.
600+ deleteResponse := githubv4mock .DataResponse (map [string ]any {
601+ "deleteIssueFieldValue" : map [string ]any {
602+ "issue" : map [string ]any {"number" : 42 },
603+ },
604+ })
605+
606+ gqlClient := githubv4 .NewClient (githubv4mock .NewMockedHTTPClient (
607+ githubv4mock .NewQueryMatcher (
608+ struct {
609+ Repository struct {
610+ Issue struct {
611+ IssueFieldValues struct {
612+ Nodes []IssueFieldValueFragment
613+ } `graphql:"issueFieldValues(first: 25)"`
614+ } `graphql:"issue(number: $number)"`
615+ } `graphql:"repository(owner: $owner, name: $repo)"`
616+ }{},
617+ map [string ]any {
618+ "owner" : githubv4 .String ("owner" ),
619+ "repo" : githubv4 .String ("repo" ),
620+ "number" : githubv4 .Int (42 ),
621+ },
622+ existingFieldsResponse ,
623+ ),
624+ githubv4mock .NewQueryMatcher (
625+ struct {
626+ Repository struct {
627+ Issue struct {
628+ ID githubv4.ID
629+ } `graphql:"issue(number: $issueNumber)"`
630+ } `graphql:"repository(owner: $owner, name: $repo)"`
631+ }{},
632+ map [string ]any {
633+ "owner" : githubv4 .String ("owner" ),
634+ "repo" : githubv4 .String ("repo" ),
635+ "issueNumber" : githubv4 .Int (42 ),
636+ },
637+ issueIDResponse ,
638+ ),
639+ githubv4mock .NewMutationMatcher (
640+ struct {
641+ DeleteIssueFieldValue struct {
642+ Issue struct {
643+ Number githubv4.Int
644+ }
645+ } `graphql:"deleteIssueFieldValue(input: $input)"`
646+ }{},
647+ DeleteIssueFieldValueInput {
648+ IssueID : githubv4 .ID ("I_node_42" ),
649+ FieldID : githubv4 .ID ("IFMS_node_101" ),
650+ },
651+ nil ,
652+ deleteResponse ,
653+ ),
654+ ))
655+
656+ result , err := UpdateIssue (
657+ context .Background (),
658+ restClient ,
659+ gqlClient ,
660+ "owner" , "repo" , 42 ,
661+ "" , "" , nil , nil , 0 , "" ,
662+ []* gogithub.IssueRequestFieldValue {
663+ {FieldID : 202 , Value : "P2" },
664+ },
665+ []fieldDeletion {{DatabaseID : 101 , NodeID : githubv4 .ID ("IFMS_node_101" )}},
666+ "" , "" , 0 ,
667+ )
668+ require .NoError (t , err )
669+ if result .IsError {
670+ t .Fatalf ("expected non-error result, got: %s" , getTextResult (t , result ).Text )
671+ }
672+ }
0 commit comments