-
Notifications
You must be signed in to change notification settings - Fork 418
Enforce minimum create_issue body length in safe outputs schema and validator
#38114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
0776993
c9274ae
786a7e7
9bc3e93
b8637db
7a7a915
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ const SAMPLE_VALIDATION_CONFIG = { | |
| defaultMax: 1, | ||
| fields: { | ||
| title: { required: true, type: "string", sanitize: true, maxLength: 128 }, | ||
| body: { required: true, type: "string", sanitize: true, maxLength: 65000 }, | ||
| body: { required: true, type: "string", sanitize: true, maxLength: 65000, minLength: 20 }, | ||
| labels: { type: "array", itemType: "string", itemSanitize: true, itemMaxLength: 128 }, | ||
| parent: { issueOrPRNumber: true }, | ||
| temporary_id: { type: "string" }, | ||
|
|
@@ -196,7 +196,7 @@ describe("safe_output_type_validator", () => { | |
| it("should validate create_issue with all required fields", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "create_issue", title: "Test Issue", body: "Test body" }, "create_issue", 1); | ||
| const result = validateItem({ type: "create_issue", title: "Test Issue", body: "Detailed issue body text." }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(true); | ||
| expect(result.normalizedItem).toBeDefined(); | ||
|
|
@@ -205,7 +205,7 @@ describe("safe_output_type_validator", () => { | |
| it("should fail validation when required title is missing", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "create_issue", body: "Test body" }, "create_issue", 1); | ||
| const result = validateItem({ type: "create_issue", body: "Detailed issue body text." }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(false); | ||
| expect(result.error).toContain("title"); | ||
|
|
@@ -223,7 +223,7 @@ describe("safe_output_type_validator", () => { | |
| it("should sanitize string fields", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "create_issue", title: "Test @mention Issue", body: "Test body" }, "create_issue", 1); | ||
| const result = validateItem({ type: "create_issue", title: "Test @mention Issue", body: "Detailed issue body text." }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(true); | ||
| // The sanitizeContent function converts @mentions to backticked format | ||
|
|
@@ -263,7 +263,7 @@ describe("safe_output_type_validator", () => { | |
| it("should not normalize backticked closing references when disabled", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "create_issue", title: "Test", body: "Closes `#123`" }, "create_issue", 1, { normalizeIssueClosingKeywords: false }); | ||
| const result = validateItem({ type: "create_issue", title: "Test", body: "Detailed context. Closes `#123`" }, "create_issue", 1, { normalizeIssueClosingKeywords: false }); | ||
|
|
||
| expect(result.isValid).toBe(true); | ||
| expect(result.normalizedItem.body).toContain("`#123`"); | ||
|
|
@@ -776,6 +776,35 @@ describe("safe_output_type_validator", () => { | |
| }); | ||
|
|
||
| describe("minLength validation", () => { | ||
| it("should reject create_issue body shorter than minLength", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "create_issue", title: "Test Issue", body: "Short" }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(false); | ||
| expect(result.error).toContain("too short"); | ||
| expect(result.error).toContain("20"); | ||
| }); | ||
|
|
||
| it("should accept create_issue body that meets minLength", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const body = "Detailed issue body with clear context."; | ||
| const result = validateItem({ type: "create_issue", title: "Test Issue", body }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(true); | ||
| }); | ||
|
|
||
| it("should accept create_issue body at exact minLength", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const body = "Exactly twenty chars"; | ||
| expect(body.length).toBe(20); | ||
| const result = validateItem({ type: "create_issue", title: "Test Issue", body }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(true); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The 💡 Suggested whitespace testit('should reject create_issue body that is only whitespace', async () => {
const { validateItem } = await import('./safe_output_type_validator.cjs');
// 25 spaces: raw length passes 20 but trimmed length is 0
const result = validateItem(
{ type: 'create_issue', title: 'Test Issue', body: ' ' },
'create_issue',
1,
);
expect(result.isValid).toBe(false);
expect(result.error).toContain('too short');
});Mirrors the existing |
||
|
|
||
| it("should reject body shorter than minLength", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
|
|
@@ -818,7 +847,7 @@ describe("safe_output_type_validator", () => { | |
| it("should validate array of strings", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "create_issue", title: "Test", body: "Body", labels: ["bug", "enhancement"] }, "create_issue", 1); | ||
| const result = validateItem({ type: "create_issue", title: "Test", body: "Detailed issue body text.", labels: ["bug", "enhancement"] }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(true); | ||
| expect(Array.isArray(result.normalizedItem.labels)).toBe(true); | ||
|
|
@@ -827,7 +856,7 @@ describe("safe_output_type_validator", () => { | |
| it("should reject array with non-string items", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "create_issue", title: "Test", body: "Body", labels: ["bug", 123] }, "create_issue", 1); | ||
| const result = validateItem({ type: "create_issue", title: "Test", body: "Detailed issue body text.", labels: ["bug", 123] }, "create_issue", 1); | ||
|
|
||
| expect(result.isValid).toBe(false); | ||
| expect(result.error).toContain("must contain only strings"); | ||
|
|
@@ -873,7 +902,7 @@ describe("safe_output_type_validator", () => { | |
| const item = { | ||
| type: "create_issue", | ||
| title: "Test", | ||
| body: "Body text", | ||
| body: "Detailed issue body text.", | ||
| metadata: { project: "test" }, | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| }, | ||
| "body": { | ||
| "type": "string", | ||
| "minLength": 20, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/zoom-out] This PR correctly aligns the
A follow-up to add |
||
| "description": "Detailed issue description in Markdown. Must be the final intended body \u2014 not a placeholder or test value. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate." | ||
| }, | ||
| "labels": { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| }, | ||
| "body": { | ||
| "type": "string", | ||
| "minLength": 20, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regression: 💡 Details and fix options
samples:
- title: "Bug from ${{ github.event.inputs.component }}"
body: "${{ github.event.inputs.description }}"will be rejected at |
||
| "description": "Detailed issue description in Markdown. Must be the final intended body \u2014 not a placeholder or test value. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate." | ||
| }, | ||
|
Comment on lines
12
to
17
|
||
| "labels": { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ const ( | |
| MaxBodyLength = 65000 | ||
| MaxGitHubUsernameLength = 39 | ||
| MaxGitHubTeamSlugLength = 100 | ||
| MinIssueBodyLength = 20 // Minimum body length for create_issue to prevent placeholder-only submissions | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing parity test: no 💡 Details
if bodyField.MinLength != MinDiscussionBodyLength { ... }No equivalent test was added for func TestCreateIssueBodyMinLength(t *testing.T) {
config, ok := ValidationConfig["create_issue"]
require.True(t, ok)
body, ok := config.Fields["body"]
require.True(t, ok)
if body.MinLength != MinIssueBodyLength {
t.Errorf("create_issue body MinLength = %d, want %d", body.MinLength, MinIssueBodyLength)
}
} |
||
| MinDiscussionBodyLength = 64 // Minimum body length for create_discussion to prevent placeholder-only submissions | ||
| ) | ||
|
|
||
|
|
@@ -54,7 +55,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ | |
| DefaultMax: 1, | ||
| Fields: map[string]FieldValidation{ | ||
| "title": {Required: true, Type: "string", Sanitize: true, MaxLength: 128}, | ||
| "body": {Required: true, Type: "string", Sanitize: true, MaxLength: MaxBodyLength}, | ||
| "body": {Required: true, Type: "string", Sanitize: true, MaxLength: MaxBodyLength, MinLength: MinIssueBodyLength}, | ||
|
Comment on lines
47
to
+58
|
||
| "labels": {Type: "array", ItemType: "string", ItemSanitize: true, ItemMaxLength: 128}, | ||
| "fields": {Type: "array"}, | ||
| "parent": {IssueOrPRNumber: true}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ func TestValidateSafeOutputsSamples_Valid(t *testing.T) { | |
| Samples: []map[string]any{ | ||
| { | ||
| "title": "Sample issue", | ||
| "body": "Sample body", | ||
| "body": "Sample issue body with enough detail.", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two other tests in this file were not updated and now silently test the wrong failure path. 💡 Details1. "body": "Body without title", // 18 chars — below the new minLength: 20The test's intent is to verify that a missing 2. "body": "${{ github.event.inputs.body }}", // substituted → "aw_sample" (9 chars < 20)This test verifies that a runtime expression on |
||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -214,7 +214,7 @@ func TestValidateSafeOutputsSamples_RuntimeExpressionsInNestedValues(t *testing. | |
| Samples: []map[string]any{ | ||
| { | ||
| "title": "Issue ${{ github.event.inputs.title_suffix }}", | ||
| "body": "Body", | ||
| "body": "Body with enough detail.", | ||
| "labels": []any{ | ||
| "static-label", | ||
| "${{ github.event.inputs.dynamic_label }}", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd] The boundary test confirms
== 20is accepted, but has no companion test for== 19(minLength − 1). Without it we cannot tell whether the implementation uses< 20or<= 20— a classic off-by-one gap.💡 Suggested test at minLength − 1
Pairing
== 19(reject) with== 20(accept) fully pins the boundary semantics.