Skip to content

Commit 93d9077

Browse files
fix(issues): validate issue comment input modes
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3085e59 commit 93d9077

4 files changed

Lines changed: 174 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ The following sets of tools are available:
894894
- **add_issue_comment** - Add comment to issue or pull request
895895
- **Required OAuth Scopes**: `repo`
896896
- `body`: Comment content. Required unless reaction is provided. (string, optional)
897-
- `comment_id`: The numeric ID of the issue or pull request comment to react to. Use this for reactions to comments; omit it to react to the issue or pull request itself. Cannot be combined with body. (number, optional)
897+
- `comment_id`: The numeric ID of the issue or pull request comment to react to. Use this for reactions to comments; omit it to react to the issue or pull request itself. Cannot be combined with body. (integer, optional)
898898
- `issue_number`: Issue or pull request number to comment on or react to. (number, required)
899899
- `owner`: Repository owner (string, required)
900900
- `reaction`: Emoji reaction to add. Required unless body is provided. (string, optional)

pkg/github/__toolsnaps__/add_issue_comment.snap

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,40 @@
66
},
77
"description": "Add a comment and/or reaction to a specific issue or issue comment in a GitHub repository. Use this tool with pull requests as well (in this case pass pull request number as issue_number), but only if user is not asking specifically to add or react to review comments. At least one of body or reaction is required.",
88
"inputSchema": {
9+
"anyOf": [
10+
{
11+
"required": [
12+
"body"
13+
]
14+
},
15+
{
16+
"required": [
17+
"reaction"
18+
]
19+
}
20+
],
21+
"dependentSchemas": {
22+
"comment_id": {
23+
"not": {
24+
"required": [
25+
"body"
26+
]
27+
},
28+
"required": [
29+
"reaction"
30+
]
31+
}
32+
},
933
"properties": {
1034
"body": {
1135
"description": "Comment content. Required unless reaction is provided.",
36+
"minLength": 1,
1237
"type": "string"
1338
},
1439
"comment_id": {
1540
"description": "The numeric ID of the issue or pull request comment to react to. Use this for reactions to comments; omit it to react to the issue or pull request itself. Cannot be combined with body.",
1641
"minimum": 1,
17-
"type": "number"
42+
"type": "integer"
1843
},
1944
"issue_number": {
2045
"description": "Issue or pull request number to comment on or react to.",

pkg/github/issues.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,13 +1212,14 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
12121212
Description: "Issue or pull request number to comment on or react to.",
12131213
},
12141214
"comment_id": {
1215-
Type: "number",
1215+
Type: "integer",
12161216
Description: "The numeric ID of the issue or pull request comment to react to. Use this for reactions to comments; omit it to react to the issue or pull request itself. Cannot be combined with body.",
12171217
Minimum: jsonschema.Ptr(1.0),
12181218
},
12191219
"body": {
12201220
Type: "string",
12211221
Description: "Comment content. Required unless reaction is provided.",
1222+
MinLength: jsonschema.Ptr(1),
12221223
},
12231224
"reaction": {
12241225
Type: "string",
@@ -1227,6 +1228,16 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
12271228
},
12281229
},
12291230
Required: []string{"owner", "repo", "issue_number"},
1231+
AnyOf: []*jsonschema.Schema{
1232+
{Required: []string{"body"}},
1233+
{Required: []string{"reaction"}},
1234+
},
1235+
DependentSchemas: map[string]*jsonschema.Schema{
1236+
"comment_id": {
1237+
Required: []string{"reaction"},
1238+
Not: &jsonschema.Schema{Required: []string{"body"}},
1239+
},
1240+
},
12301241
},
12311242
},
12321243
[]scopes.Scope{scopes.Repo},
@@ -1245,10 +1256,10 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
12451256
}
12461257
var commentID int64
12471258
hasCommentID := false
1248-
if _, ok := args["comment_id"]; ok {
1249-
commentID, err = RequiredBigInt(args, "comment_id")
1259+
if value, ok := args["comment_id"]; ok {
1260+
commentID, err = toInt64(value)
12501261
if err != nil {
1251-
return utils.NewToolResultError(err.Error()), nil, nil
1262+
return utils.NewToolResultError(fmt.Sprintf("parameter comment_id is not a valid number: %v", err)), nil, nil
12521263
}
12531264
if commentID < 1 {
12541265
return utils.NewToolResultError("comment_id must be greater than 0"), nil, nil
@@ -1278,6 +1289,9 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
12781289
if hasReaction && reactionContent == "" {
12791290
return utils.NewToolResultError("reaction cannot be empty when provided"), nil, nil
12801291
}
1292+
if hasReaction && !isValidIssueReaction(reactionContent) {
1293+
return utils.NewToolResultError("reaction must be one of +1, -1, laugh, confused, heart, hooray, rocket, eyes"), nil, nil
1294+
}
12811295

12821296
client, err := deps.GetClient(ctx)
12831297
if err != nil {
@@ -1372,6 +1386,15 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
13721386
})
13731387
}
13741388

1389+
func isValidIssueReaction(reaction string) bool {
1390+
switch reaction {
1391+
case "+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes":
1392+
return true
1393+
default:
1394+
return false
1395+
}
1396+
}
1397+
13751398
func issueNumberFromIssueURL(issueURL string) (int, error) {
13761399
issueNumberString := issueURL[strings.LastIndex(issueURL, "/")+1:]
13771400
issueNumber, err := strconv.Atoi(issueNumberString)

pkg/github/issues_test.go

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4738,11 +4738,10 @@ func Test_GetSubIssues(t *testing.T) {
47384738
}
47394739
}
47404740

4741-
func TestAddIssueComment(t *testing.T) {
4741+
func TestAddIssueCommentSchema(t *testing.T) {
47424742
t.Parallel()
47434743

4744-
serverTool := AddIssueComment(translations.NullTranslationHelper)
4745-
tool := serverTool.Tool
4744+
tool := AddIssueComment(translations.NullTranslationHelper).Tool
47464745
require.NoError(t, toolsnaps.Test(tool.Name, tool))
47474746

47484747
assert.Equal(t, "add_issue_comment", tool.Name)
@@ -4756,6 +4755,101 @@ func TestAddIssueComment(t *testing.T) {
47564755
assert.Contains(t, schema.Properties, "reaction")
47574756
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "issue_number"})
47584757

4758+
resolved, err := schema.Resolve(nil)
4759+
require.NoError(t, err)
4760+
4761+
baseArgs := map[string]any{
4762+
"owner": "owner",
4763+
"repo": "repo",
4764+
"issue_number": 42,
4765+
}
4766+
tests := []struct {
4767+
name string
4768+
args map[string]any
4769+
isValid bool
4770+
}{
4771+
{
4772+
name: "body-only comment",
4773+
args: map[string]any{"body": "This is a comment"},
4774+
isValid: true,
4775+
},
4776+
{
4777+
name: "issue or pull request reaction",
4778+
args: map[string]any{"reaction": "heart"},
4779+
isValid: true,
4780+
},
4781+
{
4782+
name: "comment and issue or pull request reaction",
4783+
args: map[string]any{"body": "This is a comment", "reaction": "heart"},
4784+
isValid: true,
4785+
},
4786+
{
4787+
name: "existing comment reaction",
4788+
args: map[string]any{"comment_id": 999, "reaction": "heart"},
4789+
isValid: true,
4790+
},
4791+
{
4792+
name: "missing body and reaction",
4793+
args: map[string]any{},
4794+
isValid: false,
4795+
},
4796+
{
4797+
name: "empty body",
4798+
args: map[string]any{"body": ""},
4799+
isValid: false,
4800+
},
4801+
{
4802+
name: "comment_id without reaction",
4803+
args: map[string]any{"comment_id": 999},
4804+
isValid: false,
4805+
},
4806+
{
4807+
name: "comment_id with body",
4808+
args: map[string]any{"comment_id": 999, "body": "This is a comment"},
4809+
isValid: false,
4810+
},
4811+
{
4812+
name: "comment_id with body and reaction",
4813+
args: map[string]any{"comment_id": 999, "body": "This is a comment", "reaction": "heart"},
4814+
isValid: false,
4815+
},
4816+
{
4817+
name: "zero comment_id",
4818+
args: map[string]any{"comment_id": 0, "reaction": "heart"},
4819+
isValid: false,
4820+
},
4821+
{
4822+
name: "fractional comment_id",
4823+
args: map[string]any{"comment_id": 1.5, "reaction": "heart"},
4824+
isValid: false,
4825+
},
4826+
{
4827+
name: "invalid reaction",
4828+
args: map[string]any{"reaction": "party"},
4829+
isValid: false,
4830+
},
4831+
}
4832+
4833+
for _, tc := range tests {
4834+
t.Run(tc.name, func(t *testing.T) {
4835+
t.Parallel()
4836+
4837+
args := maps.Clone(baseArgs)
4838+
maps.Copy(args, tc.args)
4839+
err := resolved.Validate(args)
4840+
if tc.isValid {
4841+
require.NoError(t, err)
4842+
return
4843+
}
4844+
require.Error(t, err)
4845+
})
4846+
}
4847+
}
4848+
4849+
func TestAddIssueCommentHandler(t *testing.T) {
4850+
t.Parallel()
4851+
4852+
serverTool := AddIssueComment(translations.NullTranslationHelper)
47594853
mockComment := &github.IssueComment{
47604854
ID: github.Ptr(int64(456)),
47614855
Body: github.Ptr("This is a comment"),
@@ -4908,6 +5002,18 @@ func TestAddIssueComment(t *testing.T) {
49085002
expectToolError: true,
49095003
expectedToolErrMsg: "comment_id can only be provided when reaction is provided",
49105004
},
5005+
{
5006+
name: "zero comment_id",
5007+
requestArgs: map[string]any{
5008+
"owner": "owner",
5009+
"repo": "repo",
5010+
"issue_number": float64(42),
5011+
"comment_id": float64(0),
5012+
"reaction": "heart",
5013+
},
5014+
expectToolError: true,
5015+
expectedToolErrMsg: "comment_id must be greater than 0",
5016+
},
49115017
{
49125018
name: "negative comment_id",
49135019
requestArgs: map[string]any{
@@ -4933,6 +5039,17 @@ func TestAddIssueComment(t *testing.T) {
49335039
expectToolError: true,
49345040
expectedToolErrMsg: "comment_id cannot be combined with body",
49355041
},
5042+
{
5043+
name: "invalid reaction",
5044+
requestArgs: map[string]any{
5045+
"owner": "owner",
5046+
"repo": "repo",
5047+
"issue_number": float64(42),
5048+
"reaction": "party",
5049+
},
5050+
expectToolError: true,
5051+
expectedToolErrMsg: "reaction must be one of +1, -1, laugh, confused, heart, hooray, rocket, eyes",
5052+
},
49365053
{
49375054
name: "does not create comment when reaction fails",
49385055
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{

0 commit comments

Comments
 (0)