@@ -693,6 +693,204 @@ func Test_ListIssues(t *testing.T) {
693
693
}
694
694
}
695
695
696
+ func Test_UpdateIssue (t * testing.T ) {
697
+ // Verify tool definition
698
+ mockClient := github .NewClient (nil )
699
+ tool ,_ := updateIssue (mockClient ,translations .NullTranslationHelper )
700
+
701
+ assert .Equal (t ,"update_issue" ,tool .Name )
702
+ assert .NotEmpty (t ,tool .Description )
703
+ assert .Contains (t ,tool .InputSchema .Properties ,"owner" )
704
+ assert .Contains (t ,tool .InputSchema .Properties ,"repo" )
705
+ assert .Contains (t ,tool .InputSchema .Properties ,"issue_number" )
706
+ assert .Contains (t ,tool .InputSchema .Properties ,"title" )
707
+ assert .Contains (t ,tool .InputSchema .Properties ,"body" )
708
+ assert .Contains (t ,tool .InputSchema .Properties ,"state" )
709
+ assert .Contains (t ,tool .InputSchema .Properties ,"labels" )
710
+ assert .Contains (t ,tool .InputSchema .Properties ,"assignees" )
711
+ assert .Contains (t ,tool .InputSchema .Properties ,"milestone" )
712
+ assert .ElementsMatch (t ,tool .InputSchema .Required , []string {"owner" ,"repo" ,"issue_number" })
713
+
714
+ // Setup mock issue for success case
715
+ mockIssue := & github.Issue {
716
+ Number :github .Ptr (123 ),
717
+ Title :github .Ptr ("Updated Issue Title" ),
718
+ Body :github .Ptr ("Updated issue description" ),
719
+ State :github .Ptr ("closed" ),
720
+ HTMLURL :github .Ptr ("https://github.com/owner/repo/issues/123" ),
721
+ Assignees : []* github.User {{Login :github .Ptr ("assignee1" )}, {Login :github .Ptr ("assignee2" )}},
722
+ Labels : []* github.Label {{Name :github .Ptr ("bug" )}, {Name :github .Ptr ("priority" )}},
723
+ Milestone :& github.Milestone {Number :github .Ptr (5 )},
724
+ }
725
+
726
+ tests := []struct {
727
+ name string
728
+ mockedClient * http.Client
729
+ requestArgs map [string ]interface {}
730
+ expectError bool
731
+ expectedIssue * github.Issue
732
+ expectedErrMsg string
733
+ }{
734
+ {
735
+ name :"update issue with all fields" ,
736
+ mockedClient :mock .NewMockedHTTPClient (
737
+ mock .WithRequestMatchHandler (
738
+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
739
+ mockResponse (t ,http .StatusOK ,mockIssue ),
740
+ ),
741
+ ),
742
+ requestArgs :map [string ]interface {}{
743
+ "owner" :"owner" ,
744
+ "repo" :"repo" ,
745
+ "issue_number" :float64 (123 ),
746
+ "title" :"Updated Issue Title" ,
747
+ "body" :"Updated issue description" ,
748
+ "state" :"closed" ,
749
+ "labels" :"bug,priority" ,
750
+ "assignees" :"assignee1,assignee2" ,
751
+ "milestone" :float64 (5 ),
752
+ },
753
+ expectError :false ,
754
+ expectedIssue :mockIssue ,
755
+ },
756
+ {
757
+ name :"update issue with minimal fields" ,
758
+ mockedClient :mock .NewMockedHTTPClient (
759
+ mock .WithRequestMatchHandler (
760
+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
761
+ mockResponse (t ,http .StatusOK ,& github.Issue {
762
+ Number :github .Ptr (123 ),
763
+ Title :github .Ptr ("Only Title Updated" ),
764
+ HTMLURL :github .Ptr ("https://github.com/owner/repo/issues/123" ),
765
+ State :github .Ptr ("open" ),
766
+ }),
767
+ ),
768
+ ),
769
+ requestArgs :map [string ]interface {}{
770
+ "owner" :"owner" ,
771
+ "repo" :"repo" ,
772
+ "issue_number" :float64 (123 ),
773
+ "title" :"Only Title Updated" ,
774
+ },
775
+ expectError :false ,
776
+ expectedIssue :& github.Issue {
777
+ Number :github .Ptr (123 ),
778
+ Title :github .Ptr ("Only Title Updated" ),
779
+ HTMLURL :github .Ptr ("https://github.com/owner/repo/issues/123" ),
780
+ State :github .Ptr ("open" ),
781
+ },
782
+ },
783
+ {
784
+ name :"update issue fails with not found" ,
785
+ mockedClient :mock .NewMockedHTTPClient (
786
+ mock .WithRequestMatchHandler (
787
+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
788
+ http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) {
789
+ w .WriteHeader (http .StatusNotFound )
790
+ _ ,_ = w .Write ([]byte (`{"message": "Issue not found"}` ))
791
+ }),
792
+ ),
793
+ ),
794
+ requestArgs :map [string ]interface {}{
795
+ "owner" :"owner" ,
796
+ "repo" :"repo" ,
797
+ "issue_number" :float64 (999 ),
798
+ "title" :"This issue doesn't exist" ,
799
+ },
800
+ expectError :true ,
801
+ expectedErrMsg :"failed to update issue" ,
802
+ },
803
+ {
804
+ name :"update issue fails with validation error" ,
805
+ mockedClient :mock .NewMockedHTTPClient (
806
+ mock .WithRequestMatchHandler (
807
+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
808
+ http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) {
809
+ w .WriteHeader (http .StatusUnprocessableEntity )
810
+ _ ,_ = w .Write ([]byte (`{"message": "Invalid state value"}` ))
811
+ }),
812
+ ),
813
+ ),
814
+ requestArgs :map [string ]interface {}{
815
+ "owner" :"owner" ,
816
+ "repo" :"repo" ,
817
+ "issue_number" :float64 (123 ),
818
+ "state" :"invalid_state" ,
819
+ },
820
+ expectError :true ,
821
+ expectedErrMsg :"failed to update issue" ,
822
+ },
823
+ }
824
+
825
+ for _ ,tc := range tests {
826
+ t .Run (tc .name ,func (t * testing.T ) {
827
+ // Setup client with mock
828
+ client := github .NewClient (tc .mockedClient )
829
+ _ ,handler := updateIssue (client ,translations .NullTranslationHelper )
830
+
831
+ // Create call request
832
+ request := createMCPRequest (tc .requestArgs )
833
+
834
+ // Call handler
835
+ result ,err := handler (context .Background (),request )
836
+
837
+ // Verify results
838
+ if tc .expectError {
839
+ if err != nil {
840
+ assert .Contains (t ,err .Error (),tc .expectedErrMsg )
841
+ }else {
842
+ // For errors returned as part of the result, not as an error
843
+ require .NotNil (t ,result )
844
+ textContent := getTextResult (t ,result )
845
+ assert .Contains (t ,textContent .Text ,tc .expectedErrMsg )
846
+ }
847
+ return
848
+ }
849
+
850
+ require .NoError (t ,err )
851
+
852
+ // Parse the result and get the text content if no error
853
+ textContent := getTextResult (t ,result )
854
+
855
+ // Unmarshal and verify the result
856
+ var returnedIssue github.Issue
857
+ err = json .Unmarshal ([]byte (textContent .Text ),& returnedIssue )
858
+ require .NoError (t ,err )
859
+
860
+ assert .Equal (t ,* tc .expectedIssue .Number ,* returnedIssue .Number )
861
+ assert .Equal (t ,* tc .expectedIssue .Title ,* returnedIssue .Title )
862
+ assert .Equal (t ,* tc .expectedIssue .State ,* returnedIssue .State )
863
+ assert .Equal (t ,* tc .expectedIssue .HTMLURL ,* returnedIssue .HTMLURL )
864
+
865
+ if tc .expectedIssue .Body != nil {
866
+ assert .Equal (t ,* tc .expectedIssue .Body ,* returnedIssue .Body )
867
+ }
868
+
869
+ // Check assignees if expected
870
+ if tc .expectedIssue .Assignees != nil && len (tc .expectedIssue .Assignees )> 0 {
871
+ assert .Len (t ,returnedIssue .Assignees ,len (tc .expectedIssue .Assignees ))
872
+ for i ,assignee := range returnedIssue .Assignees {
873
+ assert .Equal (t ,* tc .expectedIssue .Assignees [i ].Login ,* assignee .Login )
874
+ }
875
+ }
876
+
877
+ // Check labels if expected
878
+ if tc .expectedIssue .Labels != nil && len (tc .expectedIssue .Labels )> 0 {
879
+ assert .Len (t ,returnedIssue .Labels ,len (tc .expectedIssue .Labels ))
880
+ for i ,label := range returnedIssue .Labels {
881
+ assert .Equal (t ,* tc .expectedIssue .Labels [i ].Name ,* label .Name )
882
+ }
883
+ }
884
+
885
+ // Check milestone if expected
886
+ if tc .expectedIssue .Milestone != nil {
887
+ assert .NotNil (t ,returnedIssue .Milestone )
888
+ assert .Equal (t ,* tc .expectedIssue .Milestone .Number ,* returnedIssue .Milestone .Number )
889
+ }
890
+ })
891
+ }
892
+ }
893
+
696
894
func Test_ParseISOTimestamp (t * testing.T ) {
697
895
tests := []struct {
698
896
name string