diff --git a/.github/scripts/issue-quality.cjs b/.github/scripts/issue-quality.cjs index b2c778301..b973b3a92 100644 --- a/.github/scripts/issue-quality.cjs +++ b/.github/scripts/issue-quality.cjs @@ -374,7 +374,10 @@ function detectIssueKind(issue) { // --------------------------------------------------------------------------- function isEmpty(text) { - return clean(text).length === 0; + const c = clean(text); + if (c.length === 0) return true; + // Stand-ins like "...", "…", "---" are not actionable report content. + return /^[\p{P}\p{S}\s]+$/u.test(c); } function allSameCanonical(sections) { @@ -642,6 +645,17 @@ function validateIssue(issue) { reasons.push("Both Summary and Reproduction are empty."); guidance.push("Describe what happened and how to reproduce it."); } + } else { + // Each mapped field is required on its own — a filled Summary with an + // empty / ellipsis Reproduction (e.g. #598) must not pass. + if (isEmpty(summary)) { + reasons.push("Summary is empty."); + guidance.push("Describe what happened (the symptom or error)."); + } + if (isEmpty(repro)) { + reasons.push("Reproduction is empty."); + guidance.push("List the exact steps to reproduce the problem."); + } } // Required environment fields removed after submission. diff --git a/.github/scripts/issue-quality.test.cjs b/.github/scripts/issue-quality.test.cjs index c6a58996a..6dbe61e3c 100644 --- a/.github/scripts/issue-quality.test.cjs +++ b/.github/scripts/issue-quality.test.cjs @@ -362,8 +362,8 @@ describe("validateIssue - feature", () => { assert.equal(result.kind, "feature"); assert.equal(result.valid, false, `Expected terse goal "${goal}" to be invalid`); assert.ok( - result.reasons.some((r) => r.includes("too vague")), - `Expected too vague reason for "${goal}", got: ${result.reasons.join(", ")}`, + result.reasons.some((r) => r.includes("too vague") || /missing or empty/i.test(r)), + `Expected too vague or empty reason for "${goal}", got: ${result.reasons.join(", ")}`, ); } }); @@ -536,6 +536,70 @@ describe("validateIssue - bug", () => { assert.equal(result.valid, false); }); + it("rejects a bug with Summary filled but Reproduction empty", () => { + const body = [ + "### Client or integration", + "Codex CLI", + "### Area", + "CLI", + "### Summary", + "The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account.", + "### Reproduction", + "No response", + "### Version", + "2.7.42", + "### Operating system", + "macOS", + ].join("\n"); + const result = validateIssue({ title: "Open Codex Error", body, labels: ["bug"] }); + assert.equal(result.kind, "bug"); + assert.equal(result.valid, false); + assert.ok(result.reasons.some((r) => /Reproduction is empty/i.test(r))); + assert.ok(!result.reasons.some((r) => /Summary is empty/i.test(r))); + }); + + it("rejects a bug whose Reproduction is only an ellipsis (#598)", () => { + const body = [ + "### Client or integration", + "Codex CLI", + "### Area", + "CLI", + "### Summary", + "{\"detail\":\"The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account.\"}", + "### Reproduction", + "...", + "### Version", + "2.7.42", + "### Operating system", + "mac os", + ].join("\n"); + const result = validateIssue({ title: "Open Codex Error", body, labels: ["bug"] }); + assert.equal(result.kind, "bug"); + assert.equal(result.valid, false); + assert.ok(result.reasons.some((r) => /Reproduction is empty/i.test(r))); + }); + + it("rejects a bug with Reproduction filled but Summary empty", () => { + const body = [ + "### Client or integration", + "Codex CLI", + "### Area", + "CLI", + "### Summary", + "", + "### Reproduction", + "1. Run ocx start\n2. Send a request", + "### Version", + "2.7.42", + "### Operating system", + "macOS", + ].join("\n"); + const result = validateIssue({ title: "Crash", body, labels: ["bug"] }); + assert.equal(result.kind, "bug"); + assert.equal(result.valid, false); + assert.ok(result.reasons.some((r) => /Summary is empty/i.test(r))); + }); + it("accepts a terse real crash report", () => { const body = [ "### Client or integration",