Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/scoring/pending-pr-scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export type ContributorRepoOpenPrSignals = {

const STALE_DAYS = 14;

// Real draft markers only — "[draft]", "Draft:", "Draft -"; the delimiter keeps "Drafting" and
// "draft-js" from matching. Trailing \s* lets the same pattern also strip the marker for dedup keys.
export const DRAFT_TITLE_PATTERN = /^(?:\[\s*draft\s*\]|draft(?:\s*:|\s+-))\s*/i;

export async function loadContributorRepoOpenPrSignalRecords(
env: Env,
repoFullName: string,
Expand Down Expand Up @@ -221,8 +225,7 @@ export function applyPendingPrDetectionToScoreInput(

function isDraftPullRequest(pr: PullRequestRecord): boolean {
if (pr.isDraft) return true;
const title = pr.title.trim();
if (/^\[?\s*draft\s*\]?/i.test(title) || /^draft:/i.test(title)) return true;
if (DRAFT_TITLE_PATTERN.test(pr.title.trim())) return true;
return pr.labels.some((label) => label.toLowerCase() === "draft" || label.toLowerCase() === "wip");
}

Expand Down
3 changes: 2 additions & 1 deletion src/signals/contributor-open-pr-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { sanitizePublicComment } from "../github/commands";
import {
classifyOpenPullRequest,
detectPendingPrScenario,
DRAFT_TITLE_PATTERN,
loadContributorRepoOpenPrSignals,
type ClassifiedOpenPullRequest,
type PendingPrScenarioDetection,
Expand Down Expand Up @@ -243,7 +244,7 @@ function duplicatePronePullNumbers(openPullRequests: PullRequestRecord[]): Set<n
function normalizeTitle(title: string): string {
return title
.toLowerCase()
.replace(/^\[?\s*draft\s*\]?\s*/i, "")
.replace(DRAFT_TITLE_PATTERN, "")
.replace(/^wip:\s*/i, "")
.replace(/[^a-z0-9]+/g, " ")
.trim();
Expand Down
20 changes: 20 additions & 0 deletions test/unit/contributor-open-pr-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,26 @@ describe("contributor open PR monitor", () => {
expect(__contributorOpenPrMonitorInternals.duplicatePronePullNumbers([pr({ number: 42, labels: ["wip"] })]).has(42)).toBe(true);
});

it("strips only genuine draft markers when normalizing titles for dedup (regression)", () => {
const { duplicatePronePullNumbers } = __contributorOpenPrMonitorInternals;

// "[draft] X" and "X" are the same work, so stripping the marker collapses them into one cluster.
const clustered = duplicatePronePullNumbers([
pr({ number: 60, title: "[draft] fix parser bug" }),
pr({ number: 61, title: "fix parser bug" }),
]);
expect(clustered.has(60)).toBe(true);
expect(clustered.has(61)).toBe(true);

// A title that merely starts with the word "draft" keeps it, so it is not mistaken for "tooling".
const distinct = duplicatePronePullNumbers([
pr({ number: 62, title: "Draft tooling" }),
pr({ number: 63, title: "tooling" }),
]);
expect(distinct.has(62)).toBe(false);
expect(distinct.has(63)).toBe(false);
});

it("covers monitor summaries, guidance, next steps, and file heuristics", () => {
const { nextStepsForClassification, summarizeMonitor, buildMonitorGuidance, missingTestsFromFiles, priorityRank } =
__contributorOpenPrMonitorInternals;
Expand Down
29 changes: 29 additions & 0 deletions test/unit/pending-pr-scenarios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,35 @@ describe("pending PR scenario detection", () => {
expect(withOverlapFlags.reasons.join(" ")).toMatch(/duplicate|test files/i);
});

it("does not treat titles that merely start with the letters 'draft' as drafts (regression)", () => {
// These all begin with "draft" but carry no real marker, so they must flow through to merge_ready
// instead of being dropped — otherwise pendingMergedPrCount is silently understated. The hyphenated
// names ("draft-js", "draft-mode") are the cases an unspaced `draft[-:]` boundary would mis-flag.
for (const title of ["Drafting a new feature", "Draftsman tool", "Drafted changes", "draft-js upgrade", "Draft-mode rendering rewrite"]) {
expect(
classifyOpenPullRequest({
pr: pr({ number: 60, title }),
roleContext: outsideContributorRole,
reviews: [approvedReview(60)],
checks: [],
}).classification,
).toBe("merge_ready");
}
});

it("treats only genuine draft markers in the title as drafts", () => {
for (const title of ["[draft] spike", "[ draft ] spike", "Draft: spike", "draft : spike", "Draft - spike", "Draft -spike"]) {
expect(
classifyOpenPullRequest({
pr: pr({ number: 61, title }),
roleContext: outsideContributorRole,
reviews: [approvedReview(61)],
checks: [],
}).classification,
).toBe("draft");
}
});

it("recognizes draft heuristics and excludes pull numbers from detection", () => {
expect(
classifyOpenPullRequest({
Expand Down
Loading