-
Notifications
You must be signed in to change notification settings - Fork 431
Accept milestone_title in assign_milestone safe-output validation
#37529
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 all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,15 @@ const SAMPLE_VALIDATION_CONFIG = { | |
| agent: { type: "string", sanitize: true, maxLength: 128 }, | ||
| }, | ||
| }, | ||
| assign_milestone: { | ||
| defaultMax: 1, | ||
| customValidation: "requiresOneOf:milestone_number,milestone_title", | ||
| fields: { | ||
| issue_number: { issueNumberOrTemporaryId: true }, | ||
| milestone_number: { optionalPositiveInteger: true }, | ||
| milestone_title: { type: "string", sanitize: true, maxLength: 128 }, | ||
| }, | ||
| }, | ||
| create_pull_request_review_comment: { | ||
| defaultMax: 1, | ||
| customValidation: "startLineLessOrEqualLine", | ||
|
|
@@ -648,6 +657,27 @@ describe("safe_output_type_validator", () => { | |
| expect(result.error).toContain("requires at least one of"); | ||
| }); | ||
|
|
||
| it("should validate assign_milestone with milestone_title only", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "assign_milestone", issue_number: 42, milestone_title: "v1.0" }, "assign_milestone", 1); | ||
|
|
||
| expect(result.isValid).toBe(true); | ||
| expect(result.normalizedItem).toBeDefined(); | ||
| expect(result.normalizedItem.milestone_title).toBe("v1.0"); | ||
| }); | ||
|
|
||
| it("should fail assign_milestone when both milestone_number and milestone_title are missing", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
| const result = validateItem({ type: "assign_milestone", issue_number: 42 }, "assign_milestone", 1); | ||
|
|
||
| expect(result.isValid).toBe(false); | ||
| expect(result.error).toContain("requires at least one of"); | ||
| expect(result.error).toContain("milestone_number"); | ||
| expect(result.error).toContain("milestone_title"); | ||
| }); | ||
|
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 testit("should validate assign_milestone when both milestone_number and milestone_title are provided", async () => {
const { validateItem } = await import("./safe_output_type_validator.cjs");
const result = validateItem(
{ type: "assign_milestone", issue_number: 42, milestone_number: 3, milestone_title: "v1.0" },
"assign_milestone",
1
);
expect(result.isValid).toBe(true);
});Documenting this case also clarifies what the downstream handler should do when both fields are present (prefer |
||
|
|
||
| it("should pass for update_pull_request when update_branch is true", async () => { | ||
| const { validateItem } = await import("./safe_output_type_validator.cjs"); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -266,15 +266,16 @@ func TestUpdatePullRequestValidationConfig(t *testing.T) { | |
| func TestValidationConfigConsistency(t *testing.T) { | ||
| // Verify that all types with customValidation have valid validation rules | ||
| validCustomValidations := map[string]bool{ | ||
| "requiresOneOf:status,title,body": true, | ||
| "requiresOneOf:title,body": true, | ||
| "requiresOneOf:title,body,update_branch": true, | ||
| "requiresOneOf:title,body,labels": true, | ||
| "requiresOneOf:issue_number,pull_number": true, | ||
| "requiresOneOf:field_name,field_node_id": true, | ||
| "requiresOneOf:reviewers,team_reviewers": true, | ||
| "startLineLessOrEqualLine": true, | ||
| "parentAndSubDifferent": true, | ||
| "requiresOneOf:status,title,body": true, | ||
| "requiresOneOf:title,body": true, | ||
| "requiresOneOf:title,body,update_branch": true, | ||
| "requiresOneOf:title,body,labels": true, | ||
| "requiresOneOf:issue_number,pull_number": true, | ||
| "requiresOneOf:milestone_number,milestone_title": true, | ||
| "requiresOneOf:field_name,field_node_id": true, | ||
| "requiresOneOf:reviewers,team_reviewers": true, | ||
| "startLineLessOrEqualLine": true, | ||
| "parentAndSubDifferent": true, | ||
| } | ||
|
|
||
| for typeName, config := range ValidationConfig { | ||
|
|
@@ -353,6 +354,44 @@ func TestCreatePullRequestBaseValidationMaxLength(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestAssignMilestoneValidationConfig(t *testing.T) { | ||
| config, ok := ValidationConfig["assign_milestone"] | ||
| if !ok { | ||
| t.Fatal("assign_milestone not found in ValidationConfig") | ||
| } | ||
|
|
||
| if config.CustomValidation != "requiresOneOf:milestone_number,milestone_title" { | ||
| t.Errorf("assign_milestone customValidation = %q, want %q", config.CustomValidation, "requiresOneOf:milestone_number,milestone_title") | ||
| } | ||
|
|
||
| if _, ok := config.Fields["milestone_number"]; !ok { | ||
| t.Error("assign_milestone Fields is missing the 'milestone_number' field") | ||
| } | ||
| if _, ok := config.Fields["milestone_title"]; !ok { | ||
| t.Error("assign_milestone Fields is missing the 'milestone_title' field") | ||
|
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] 💡 What to addfunc TestAssignMilestoneFieldValidation(t *testing.T) {
cfg := ValidationConfig["assign_milestone"]
milestoneNumField := cfg.Fields["milestone_number"]
// OptionalPositiveInteger: absent is fine, but 0 must be rejected
if milestoneNumField.OptionalPositiveInteger != true {
t.Error("milestone_number should be OptionalPositiveInteger")
}
// milestone_title: must have sanitize + maxLength
milestoneTitle := cfg.Fields["milestone_title"]
if milestoneTitle.MaxLength != 128 {
t.Errorf("milestone_title MaxLength = %d, want 128", milestoneTitle.MaxLength)
}
if !milestoneTitle.Sanitize {
t.Error("milestone_title should have Sanitize=true")
}
}This level of coverage catches a future maintainer accidentally weakening field-level validation while keeping the struct keys intact. |
||
| } | ||
| } | ||
|
|
||
| func TestAssignMilestoneValidationConfigJSON(t *testing.T) { | ||
| jsonStr, err := GetValidationConfigJSON([]string{"assign_milestone"}, nil) | ||
| if err != nil { | ||
| t.Fatalf("GetValidationConfigJSON() error = %v", err) | ||
| } | ||
|
|
||
| var parsed map[string]TypeValidationConfig | ||
| if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil { | ||
| t.Fatalf("Failed to parse validation config JSON: %v", err) | ||
| } | ||
|
|
||
| cfg, ok := parsed["assign_milestone"] | ||
| if !ok { | ||
| t.Fatal("assign_milestone not found in serialized validation config") | ||
| } | ||
| if cfg.CustomValidation != "requiresOneOf:milestone_number,milestone_title" { | ||
| t.Errorf("assign_milestone customValidation in JSON = %q, want %q", cfg.CustomValidation, "requiresOneOf:milestone_number,milestone_title") | ||
| } | ||
| } | ||
|
|
||
| func TestBuildValidationConfigCacheKey(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,10 +118,12 @@ var ValidationConfig = map[string]TypeValidationConfig{ | |
| }, | ||
| }, | ||
| "assign_milestone": { | ||
| DefaultMax: 1, | ||
| DefaultMax: 1, | ||
| CustomValidation: "requiresOneOf:milestone_number,milestone_title", | ||
|
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. [/diagnose] The bug description states items were silently dropped before reaching the handler that resolves 💡 What to addA test in the
This turns the regression description in the PR body into a verifiable spec that prevents the same silent-drop pattern from reappearing in the handler layer. |
||
| Fields: map[string]FieldValidation{ | ||
| "issue_number": {IssueNumberOrTemporaryID: true}, | ||
| "milestone_number": {Required: true, PositiveInteger: true}, | ||
| "milestone_number": {OptionalPositiveInteger: true}, | ||
| "milestone_title": {Type: "string", Sanitize: true, MaxLength: 128}, | ||
|
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. Validation accepts 💡 Suggested fixAdd "milestone_title": {Type: "string", Sanitize: true, MaxLength: 128, MinLength: 1},Mirror it in the JS test config ( milestone_title: { type: "string", sanitize: true, maxLength: 128, minLength: 1 },The |
||
| "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" | ||
| }, | ||
| }, | ||
|
|
||
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
milestone_title-only path is now exercised, but the original happy path —milestone_numberalone — has no explicit regression test. Sincemilestone_numberchanged from{Required: true, PositiveInteger: true}to{OptionalPositiveInteger: true}, a test confirming numeric milestone references still validate correctly provides a clear regression guard.💡 Suggested test
This guards the original use case and confirms the field-level refactor from
PositiveIntegertoOptionalPositiveIntegerdid not silently change acceptance criteria for the pre-existing input shape.