@@ -2120,3 +2120,267 @@ func Test_ListSubIssues(t *testing.T) {
2120
2120
})
2121
2121
}
2122
2122
}
2123
+
2124
+ func Test_RemoveSubIssue (t * testing.T ) {
2125
+ // Verify tool definition once
2126
+ mockClient := github .NewClient (nil )
2127
+ tool ,_ := RemoveSubIssue (stubGetClientFn (mockClient ),translations .NullTranslationHelper )
2128
+
2129
+ assert .Equal (t ,"remove_sub_issue" ,tool .Name )
2130
+ assert .NotEmpty (t ,tool .Description )
2131
+ assert .Contains (t ,tool .InputSchema .Properties ,"owner" )
2132
+ assert .Contains (t ,tool .InputSchema .Properties ,"repo" )
2133
+ assert .Contains (t ,tool .InputSchema .Properties ,"issue_number" )
2134
+ assert .Contains (t ,tool .InputSchema .Properties ,"sub_issue_id" )
2135
+ assert .ElementsMatch (t ,tool .InputSchema .Required , []string {"owner" ,"repo" ,"issue_number" ,"sub_issue_id" })
2136
+
2137
+ // Setup mock issue for success case (matches GitHub API response format - the updated parent issue)
2138
+ mockIssue := & github.Issue {
2139
+ Number :github .Ptr (42 ),
2140
+ Title :github .Ptr ("Parent Issue" ),
2141
+ Body :github .Ptr ("This is the parent issue after sub-issue removal" ),
2142
+ State :github .Ptr ("open" ),
2143
+ HTMLURL :github .Ptr ("https://github.com/owner/repo/issues/42" ),
2144
+ User :& github.User {
2145
+ Login :github .Ptr ("testuser" ),
2146
+ },
2147
+ Labels : []* github.Label {
2148
+ {
2149
+ Name :github .Ptr ("enhancement" ),
2150
+ Color :github .Ptr ("84b6eb" ),
2151
+ Description :github .Ptr ("New feature or request" ),
2152
+ },
2153
+ },
2154
+ }
2155
+
2156
+ tests := []struct {
2157
+ name string
2158
+ mockedClient * http.Client
2159
+ requestArgs map [string ]interface {}
2160
+ expectError bool
2161
+ expectedIssue * github.Issue
2162
+ expectedErrMsg string
2163
+ }{
2164
+ {
2165
+ name :"successful sub-issue removal" ,
2166
+ mockedClient :mock .NewMockedHTTPClient (
2167
+ mock .WithRequestMatchHandler (
2168
+ mock.EndpointPattern {
2169
+ Pattern :"/repos/owner/repo/issues/42/sub_issue" ,
2170
+ Method :"DELETE" ,
2171
+ },
2172
+ expectRequestBody (t ,map [string ]interface {}{
2173
+ "sub_issue_id" :float64 (123 ),
2174
+ }).andThen (
2175
+ mockResponse (t ,http .StatusOK ,mockIssue ),
2176
+ ),
2177
+ ),
2178
+ ),
2179
+ requestArgs :map [string ]interface {}{
2180
+ "owner" :"owner" ,
2181
+ "repo" :"repo" ,
2182
+ "issue_number" :float64 (42 ),
2183
+ "sub_issue_id" :float64 (123 ),
2184
+ },
2185
+ expectError :false ,
2186
+ expectedIssue :mockIssue ,
2187
+ },
2188
+ {
2189
+ name :"parent issue not found" ,
2190
+ mockedClient :mock .NewMockedHTTPClient (
2191
+ mock .WithRequestMatchHandler (
2192
+ mock.EndpointPattern {
2193
+ Pattern :"/repos/owner/repo/issues/999/sub_issue" ,
2194
+ Method :"DELETE" ,
2195
+ },
2196
+ http .HandlerFunc (func (w http.ResponseWriter ,_ * http.Request ) {
2197
+ w .WriteHeader (http .StatusNotFound )
2198
+ _ ,_ = w .Write ([]byte (`{"message": "Not Found"}` ))
2199
+ }),
2200
+ ),
2201
+ ),
2202
+ requestArgs :map [string ]interface {}{
2203
+ "owner" :"owner" ,
2204
+ "repo" :"repo" ,
2205
+ "issue_number" :float64 (999 ),
2206
+ "sub_issue_id" :float64 (123 ),
2207
+ },
2208
+ expectError :false ,
2209
+ expectedErrMsg :"failed to remove sub-issue" ,
2210
+ },
2211
+ {
2212
+ name :"sub-issue not found" ,
2213
+ mockedClient :mock .NewMockedHTTPClient (
2214
+ mock .WithRequestMatchHandler (
2215
+ mock.EndpointPattern {
2216
+ Pattern :"/repos/owner/repo/issues/42/sub_issue" ,
2217
+ Method :"DELETE" ,
2218
+ },
2219
+ http .HandlerFunc (func (w http.ResponseWriter ,_ * http.Request ) {
2220
+ w .WriteHeader (http .StatusNotFound )
2221
+ _ ,_ = w .Write ([]byte (`{"message": "Sub-issue not found"}` ))
2222
+ }),
2223
+ ),
2224
+ ),
2225
+ requestArgs :map [string ]interface {}{
2226
+ "owner" :"owner" ,
2227
+ "repo" :"repo" ,
2228
+ "issue_number" :float64 (42 ),
2229
+ "sub_issue_id" :float64 (999 ),
2230
+ },
2231
+ expectError :false ,
2232
+ expectedErrMsg :"failed to remove sub-issue" ,
2233
+ },
2234
+ {
2235
+ name :"bad request - invalid sub_issue_id" ,
2236
+ mockedClient :mock .NewMockedHTTPClient (
2237
+ mock .WithRequestMatchHandler (
2238
+ mock.EndpointPattern {
2239
+ Pattern :"/repos/owner/repo/issues/42/sub_issue" ,
2240
+ Method :"DELETE" ,
2241
+ },
2242
+ http .HandlerFunc (func (w http.ResponseWriter ,_ * http.Request ) {
2243
+ w .WriteHeader (http .StatusBadRequest )
2244
+ _ ,_ = w .Write ([]byte (`{"message": "Invalid sub_issue_id"}` ))
2245
+ }),
2246
+ ),
2247
+ ),
2248
+ requestArgs :map [string ]interface {}{
2249
+ "owner" :"owner" ,
2250
+ "repo" :"repo" ,
2251
+ "issue_number" :float64 (42 ),
2252
+ "sub_issue_id" :float64 (- 1 ),
2253
+ },
2254
+ expectError :false ,
2255
+ expectedErrMsg :"failed to remove sub-issue" ,
2256
+ },
2257
+ {
2258
+ name :"repository not found" ,
2259
+ mockedClient :mock .NewMockedHTTPClient (
2260
+ mock .WithRequestMatchHandler (
2261
+ mock.EndpointPattern {
2262
+ Pattern :"/repos/nonexistent/repo/issues/42/sub_issue" ,
2263
+ Method :"DELETE" ,
2264
+ },
2265
+ http .HandlerFunc (func (w http.ResponseWriter ,_ * http.Request ) {
2266
+ w .WriteHeader (http .StatusNotFound )
2267
+ _ ,_ = w .Write ([]byte (`{"message": "Not Found"}` ))
2268
+ }),
2269
+ ),
2270
+ ),
2271
+ requestArgs :map [string ]interface {}{
2272
+ "owner" :"nonexistent" ,
2273
+ "repo" :"repo" ,
2274
+ "issue_number" :float64 (42 ),
2275
+ "sub_issue_id" :float64 (123 ),
2276
+ },
2277
+ expectError :false ,
2278
+ expectedErrMsg :"failed to remove sub-issue" ,
2279
+ },
2280
+ {
2281
+ name :"insufficient permissions" ,
2282
+ mockedClient :mock .NewMockedHTTPClient (
2283
+ mock .WithRequestMatchHandler (
2284
+ mock.EndpointPattern {
2285
+ Pattern :"/repos/owner/repo/issues/42/sub_issue" ,
2286
+ Method :"DELETE" ,
2287
+ },
2288
+ http .HandlerFunc (func (w http.ResponseWriter ,_ * http.Request ) {
2289
+ w .WriteHeader (http .StatusForbidden )
2290
+ _ ,_ = w .Write ([]byte (`{"message": "Must have write access to repository"}` ))
2291
+ }),
2292
+ ),
2293
+ ),
2294
+ requestArgs :map [string ]interface {}{
2295
+ "owner" :"owner" ,
2296
+ "repo" :"repo" ,
2297
+ "issue_number" :float64 (42 ),
2298
+ "sub_issue_id" :float64 (123 ),
2299
+ },
2300
+ expectError :false ,
2301
+ expectedErrMsg :"failed to remove sub-issue" ,
2302
+ },
2303
+ {
2304
+ name :"missing required parameter owner" ,
2305
+ mockedClient :mock .NewMockedHTTPClient (
2306
+ mock .WithRequestMatchHandler (
2307
+ mock.EndpointPattern {
2308
+ Pattern :"/repos/owner/repo/issues/42/sub_issue" ,
2309
+ Method :"DELETE" ,
2310
+ },
2311
+ mockResponse (t ,http .StatusOK ,mockIssue ),
2312
+ ),
2313
+ ),
2314
+ requestArgs :map [string ]interface {}{
2315
+ "repo" :"repo" ,
2316
+ "issue_number" :float64 (42 ),
2317
+ "sub_issue_id" :float64 (123 ),
2318
+ },
2319
+ expectError :false ,
2320
+ expectedErrMsg :"missing required parameter: owner" ,
2321
+ },
2322
+ {
2323
+ name :"missing required parameter sub_issue_id" ,
2324
+ mockedClient :mock .NewMockedHTTPClient (
2325
+ mock .WithRequestMatchHandler (
2326
+ mock.EndpointPattern {
2327
+ Pattern :"/repos/owner/repo/issues/42/sub_issue" ,
2328
+ Method :"DELETE" ,
2329
+ },
2330
+ mockResponse (t ,http .StatusOK ,mockIssue ),
2331
+ ),
2332
+ ),
2333
+ requestArgs :map [string ]interface {}{
2334
+ "owner" :"owner" ,
2335
+ "repo" :"repo" ,
2336
+ "issue_number" :float64 (42 ),
2337
+ },
2338
+ expectError :false ,
2339
+ expectedErrMsg :"missing required parameter: sub_issue_id" ,
2340
+ },
2341
+ }
2342
+
2343
+ for _ ,tc := range tests {
2344
+ t .Run (tc .name ,func (t * testing.T ) {
2345
+ // Setup client with mock
2346
+ client := github .NewClient (tc .mockedClient )
2347
+ _ ,handler := RemoveSubIssue (stubGetClientFn (client ),translations .NullTranslationHelper )
2348
+
2349
+ // Create call request
2350
+ request := createMCPRequest (tc .requestArgs )
2351
+
2352
+ // Call handler
2353
+ result ,err := handler (context .Background (),request )
2354
+
2355
+ // Verify results
2356
+ if tc .expectError {
2357
+ require .Error (t ,err )
2358
+ assert .Contains (t ,err .Error (),tc .expectedErrMsg )
2359
+ return
2360
+ }
2361
+
2362
+ if tc .expectedErrMsg != "" {
2363
+ require .NotNil (t ,result )
2364
+ textContent := getTextResult (t ,result )
2365
+ assert .Contains (t ,textContent .Text ,tc .expectedErrMsg )
2366
+ return
2367
+ }
2368
+
2369
+ require .NoError (t ,err )
2370
+
2371
+ // Parse the result and get the text content if no error
2372
+ textContent := getTextResult (t ,result )
2373
+
2374
+ // Unmarshal and verify the result
2375
+ var returnedIssue github.Issue
2376
+ err = json .Unmarshal ([]byte (textContent .Text ),& returnedIssue )
2377
+ require .NoError (t ,err )
2378
+ assert .Equal (t ,* tc .expectedIssue .Number ,* returnedIssue .Number )
2379
+ assert .Equal (t ,* tc .expectedIssue .Title ,* returnedIssue .Title )
2380
+ assert .Equal (t ,* tc .expectedIssue .Body ,* returnedIssue .Body )
2381
+ assert .Equal (t ,* tc .expectedIssue .State ,* returnedIssue .State )
2382
+ assert .Equal (t ,* tc .expectedIssue .HTMLURL ,* returnedIssue .HTMLURL )
2383
+ assert .Equal (t ,* tc .expectedIssue .User .Login ,* returnedIssue .User .Login )
2384
+ })
2385
+ }
2386
+ }