Skip to content

Commit b971c95

Browse files
committed
feat: add reaction removal tools
Add granular tools for removing reactions from issues, issue comments, and pull request review comments, with destructive annotations, tests, snapshots, and generated documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9acf9c83-49aa-41af-a42d-ad75de34d132
1 parent 7d13a7a commit b971c95

9 files changed

Lines changed: 550 additions & 0 deletions

‎docs/feature-flags.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,20 @@ as output formatting) won't appear here.
184184
- `repo`: Repository name (string, required)
185185
- `title`: Issue title (string, required)
186186

187+
- **remove_issue_comment_reaction** - Remove Reaction from Issue or Pull Request Comment
188+
- **OAuth Challenge Scopes**: `repo`
189+
- `comment_id`: The issue or pull request comment ID (number, required)
190+
- `owner`: Repository owner (username or organization) (string, required)
191+
- `reaction_id`: The reaction ID to remove (number, required)
192+
- `repo`: Repository name (string, required)
193+
194+
- **remove_issue_reaction** - Remove Reaction from Issue or Pull Request
195+
- **OAuth Challenge Scopes**: `repo`
196+
- `issue_number`: The issue number (number, required)
197+
- `owner`: Repository owner (username or organization) (string, required)
198+
- `reaction_id`: The reaction ID to remove (number, required)
199+
- `repo`: Repository name (string, required)
200+
187201
- **remove_sub_issue** - Remove Sub-Issue
188202
- **OAuth Challenge Scopes**: `repo`
189203
- `issue_number`: The parent issue number (number, required)
@@ -301,6 +315,13 @@ as output formatting) won't appear here.
301315
- `pullNumber`: The pull request number (number, required)
302316
- `repo`: Repository name (string, required)
303317

318+
- **remove_pull_request_review_comment_reaction** - Remove Pull Request Review Comment Reaction
319+
- **OAuth Challenge Scopes**: `repo`
320+
- `comment_id`: The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...). (number, required)
321+
- `owner`: Repository owner (username or organization) (string, required)
322+
- `reaction_id`: The reaction ID to remove (number, required)
323+
- `repo`: Repository name (string, required)
324+
304325
- **request_pull_request_reviewers** - Request Pull Request Reviewers
305326
- **OAuth Challenge Scopes**: `repo`
306327
- `owner`: Repository owner (username or organization) (string, required)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"annotations": {
3+
"destructiveHint": true,
4+
"idempotentHint": false,
5+
"openWorldHint": true,
6+
"readOnlyHint": false,
7+
"title": "Remove Reaction from Issue or Pull Request Comment"
8+
},
9+
"description": "Remove a reaction from an issue or pull request comment.",
10+
"inputSchema": {
11+
"properties": {
12+
"comment_id": {
13+
"description": "The issue or pull request comment ID",
14+
"minimum": 1,
15+
"type": "number"
16+
},
17+
"owner": {
18+
"description": "Repository owner (username or organization)",
19+
"type": "string"
20+
},
21+
"reaction_id": {
22+
"description": "The reaction ID to remove",
23+
"minimum": 1,
24+
"type": "number"
25+
},
26+
"repo": {
27+
"description": "Repository name",
28+
"type": "string"
29+
}
30+
},
31+
"required": [
32+
"owner",
33+
"repo",
34+
"comment_id",
35+
"reaction_id"
36+
],
37+
"type": "object"
38+
},
39+
"name": "remove_issue_comment_reaction"
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"annotations": {
3+
"destructiveHint": true,
4+
"idempotentHint": false,
5+
"openWorldHint": true,
6+
"readOnlyHint": false,
7+
"title": "Remove Reaction from Issue or Pull Request"
8+
},
9+
"description": "Remove a reaction from an issue or pull request.",
10+
"inputSchema": {
11+
"properties": {
12+
"issue_number": {
13+
"description": "The issue number",
14+
"minimum": 1,
15+
"type": "number"
16+
},
17+
"owner": {
18+
"description": "Repository owner (username or organization)",
19+
"type": "string"
20+
},
21+
"reaction_id": {
22+
"description": "The reaction ID to remove",
23+
"minimum": 1,
24+
"type": "number"
25+
},
26+
"repo": {
27+
"description": "Repository name",
28+
"type": "string"
29+
}
30+
},
31+
"required": [
32+
"owner",
33+
"repo",
34+
"issue_number",
35+
"reaction_id"
36+
],
37+
"type": "object"
38+
},
39+
"name": "remove_issue_reaction"
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"annotations": {
3+
"destructiveHint": true,
4+
"idempotentHint": false,
5+
"openWorldHint": true,
6+
"readOnlyHint": false,
7+
"title": "Remove Pull Request Review Comment Reaction"
8+
},
9+
"description": "Remove a reaction from a pull request review comment.",
10+
"inputSchema": {
11+
"properties": {
12+
"comment_id": {
13+
"description": "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).",
14+
"minimum": 1,
15+
"type": "number"
16+
},
17+
"owner": {
18+
"description": "Repository owner (username or organization)",
19+
"type": "string"
20+
},
21+
"reaction_id": {
22+
"description": "The reaction ID to remove",
23+
"minimum": 1,
24+
"type": "number"
25+
},
26+
"repo": {
27+
"description": "Repository name",
28+
"type": "string"
29+
}
30+
},
31+
"required": [
32+
"owner",
33+
"repo",
34+
"comment_id",
35+
"reaction_id"
36+
],
37+
"type": "object"
38+
},
39+
"name": "remove_pull_request_review_comment_reaction"
40+
}

‎pkg/github/granular_tools_test.go‎

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ func TestGranularToolSnaps(t *testing.T) {
5353
GranularReprioritizeSubIssue,
5454
GranularSetIssueFields,
5555
GranularAddIssueReaction,
56+
GranularRemoveIssueReaction,
5657
GranularAddIssueCommentReaction,
58+
GranularRemoveIssueCommentReaction,
5759
GranularUpdatePullRequestTitle,
5860
GranularUpdatePullRequestBody,
5961
GranularUpdatePullRequestState,
@@ -66,6 +68,7 @@ func TestGranularToolSnaps(t *testing.T) {
6668
GranularResolveReviewThread,
6769
GranularUnresolveReviewThread,
6870
GranularAddPullRequestReviewCommentReaction,
71+
GranularRemovePullRequestReviewCommentReaction,
6972
}
7073

7174
for _, constructor := range toolConstructors {
@@ -99,7 +102,9 @@ func TestIssuesGranularToolset(t *testing.T) {
99102
"reprioritize_sub_issue",
100103
"set_issue_fields",
101104
"add_issue_reaction",
105+
"remove_issue_reaction",
102106
"add_issue_comment_reaction",
107+
"remove_issue_comment_reaction",
103108
}
104109
for _, name := range expected {
105110
assert.Contains(t, toolNames, name)
@@ -136,6 +141,7 @@ func TestPullRequestsGranularToolset(t *testing.T) {
136141
"resolve_review_thread",
137142
"unresolve_review_thread",
138143
"add_pull_request_review_comment_reaction",
144+
"remove_pull_request_review_comment_reaction",
139145
}
140146
for _, name := range expected {
141147
assert.Contains(t, toolNames, name)
@@ -2472,6 +2478,72 @@ func TestGranularAddIssueReaction(t *testing.T) {
24722478
}
24732479
}
24742480

2481+
func TestGranularRemoveIssueReaction(t *testing.T) {
2482+
tests := []struct {
2483+
name string
2484+
mockedClient *http.Client
2485+
args map[string]any
2486+
expectedErrMsg string
2487+
}{
2488+
{
2489+
name: "remove reaction from issue successfully",
2490+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
2491+
DeleteReposIssuesReactionsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNoContent, nil),
2492+
}),
2493+
args: map[string]any{
2494+
"owner": "owner",
2495+
"repo": "repo",
2496+
"issue_number": float64(42),
2497+
"reaction_id": float64(12345),
2498+
},
2499+
},
2500+
{
2501+
name: "missing reaction_id returns error",
2502+
mockedClient: MockHTTPClientWithHandlers(nil),
2503+
args: map[string]any{
2504+
"owner": "owner",
2505+
"repo": "repo",
2506+
"issue_number": float64(42),
2507+
},
2508+
expectedErrMsg: "missing required parameter: reaction_id",
2509+
},
2510+
{
2511+
name: "API error",
2512+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
2513+
DeleteReposIssuesReactionsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message":"Not Found"}`),
2514+
}),
2515+
args: map[string]any{
2516+
"owner": "owner",
2517+
"repo": "repo",
2518+
"issue_number": float64(42),
2519+
"reaction_id": float64(12345),
2520+
},
2521+
expectedErrMsg: "failed to remove reaction from issue",
2522+
},
2523+
}
2524+
2525+
for _, tc := range tests {
2526+
t.Run(tc.name, func(t *testing.T) {
2527+
client := mustNewGHClient(t, tc.mockedClient)
2528+
deps := BaseDeps{Client: client}
2529+
serverTool := GranularRemoveIssueReaction(translations.NullTranslationHelper)
2530+
require.NotNil(t, serverTool.Tool.Annotations.DestructiveHint)
2531+
assert.True(t, *serverTool.Tool.Annotations.DestructiveHint)
2532+
handler := serverTool.Handler(deps)
2533+
request := createMCPRequest(tc.args)
2534+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
2535+
require.NoError(t, err)
2536+
if tc.expectedErrMsg != "" {
2537+
require.True(t, result.IsError)
2538+
assert.Contains(t, getErrorResult(t, result).Text, tc.expectedErrMsg)
2539+
return
2540+
}
2541+
require.False(t, result.IsError)
2542+
assert.Equal(t, "reaction successfully removed from issue", getTextResult(t, result).Text)
2543+
})
2544+
}
2545+
}
2546+
24752547
func TestGranularAddIssueCommentReaction(t *testing.T) {
24762548
mockReaction := &gogithub.Reaction{
24772549
ID: gogithub.Ptr(int64(67890)),
@@ -2532,6 +2604,59 @@ func TestGranularAddIssueCommentReaction(t *testing.T) {
25322604
}
25332605
}
25342606

2607+
func TestGranularRemoveIssueCommentReaction(t *testing.T) {
2608+
tests := []struct {
2609+
name string
2610+
mockedClient *http.Client
2611+
args map[string]any
2612+
expectedErrMsg string
2613+
}{
2614+
{
2615+
name: "remove reaction from issue comment successfully",
2616+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
2617+
DeleteReposIssuesCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusNoContent, nil),
2618+
}),
2619+
args: map[string]any{
2620+
"owner": "owner",
2621+
"repo": "repo",
2622+
"comment_id": float64(999),
2623+
"reaction_id": float64(67890),
2624+
},
2625+
},
2626+
{
2627+
name: "missing comment_id returns error",
2628+
mockedClient: MockHTTPClientWithHandlers(nil),
2629+
args: map[string]any{
2630+
"owner": "owner",
2631+
"repo": "repo",
2632+
"reaction_id": float64(67890),
2633+
},
2634+
expectedErrMsg: "missing required parameter: comment_id",
2635+
},
2636+
}
2637+
2638+
for _, tc := range tests {
2639+
t.Run(tc.name, func(t *testing.T) {
2640+
client := mustNewGHClient(t, tc.mockedClient)
2641+
deps := BaseDeps{Client: client}
2642+
serverTool := GranularRemoveIssueCommentReaction(translations.NullTranslationHelper)
2643+
require.NotNil(t, serverTool.Tool.Annotations.DestructiveHint)
2644+
assert.True(t, *serverTool.Tool.Annotations.DestructiveHint)
2645+
handler := serverTool.Handler(deps)
2646+
request := createMCPRequest(tc.args)
2647+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
2648+
require.NoError(t, err)
2649+
if tc.expectedErrMsg != "" {
2650+
require.True(t, result.IsError)
2651+
assert.Contains(t, getErrorResult(t, result).Text, tc.expectedErrMsg)
2652+
return
2653+
}
2654+
require.False(t, result.IsError)
2655+
assert.Equal(t, "reaction successfully removed from issue comment", getTextResult(t, result).Text)
2656+
})
2657+
}
2658+
}
2659+
25352660
func TestGranularAddPullRequestReviewCommentReaction(t *testing.T) {
25362661
mockReaction := &gogithub.Reaction{
25372662
ID: gogithub.Ptr(int64(54321)),
@@ -2591,3 +2716,56 @@ func TestGranularAddPullRequestReviewCommentReaction(t *testing.T) {
25912716
})
25922717
}
25932718
}
2719+
2720+
func TestGranularRemovePullRequestReviewCommentReaction(t *testing.T) {
2721+
tests := []struct {
2722+
name string
2723+
mockedClient *http.Client
2724+
args map[string]any
2725+
expectedErrMsg string
2726+
}{
2727+
{
2728+
name: "remove reaction from PR review comment successfully",
2729+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
2730+
DeleteReposPullsCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusNoContent, nil),
2731+
}),
2732+
args: map[string]any{
2733+
"owner": "owner",
2734+
"repo": "repo",
2735+
"comment_id": float64(888),
2736+
"reaction_id": float64(54321),
2737+
},
2738+
},
2739+
{
2740+
name: "missing repo returns error",
2741+
mockedClient: MockHTTPClientWithHandlers(nil),
2742+
args: map[string]any{
2743+
"owner": "owner",
2744+
"comment_id": float64(888),
2745+
"reaction_id": float64(54321),
2746+
},
2747+
expectedErrMsg: "missing required parameter: repo",
2748+
},
2749+
}
2750+
2751+
for _, tc := range tests {
2752+
t.Run(tc.name, func(t *testing.T) {
2753+
client := mustNewGHClient(t, tc.mockedClient)
2754+
deps := BaseDeps{Client: client}
2755+
serverTool := GranularRemovePullRequestReviewCommentReaction(translations.NullTranslationHelper)
2756+
require.NotNil(t, serverTool.Tool.Annotations.DestructiveHint)
2757+
assert.True(t, *serverTool.Tool.Annotations.DestructiveHint)
2758+
handler := serverTool.Handler(deps)
2759+
request := createMCPRequest(tc.args)
2760+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
2761+
require.NoError(t, err)
2762+
if tc.expectedErrMsg != "" {
2763+
require.True(t, result.IsError)
2764+
assert.Contains(t, getErrorResult(t, result).Text, tc.expectedErrMsg)
2765+
return
2766+
}
2767+
require.False(t, result.IsError)
2768+
assert.Equal(t, "reaction successfully removed from pull request review comment", getTextResult(t, result).Text)
2769+
})
2770+
}
2771+
}

0 commit comments

Comments
 (0)