diff --git a/.github/scripts/pr-merge/clear-escalations.mjs b/.github/scripts/pr-merge/clear-escalations.mjs index f1b04e9758..b36a852b18 100755 --- a/.github/scripts/pr-merge/clear-escalations.mjs +++ b/.github/scripts/pr-merge/clear-escalations.mjs @@ -8,6 +8,12 @@ // App bot as its actor, and the App's first escalation comment after it // came within PAIRED_MS (report-escalation.sh writes the label, then the // comment). +// - No review lane holds it: no comment carrying a REVIEW_HOLD marker line, by +// claude[bot] or by github-actions[bot] ending in the lane's `claude-lane:` +// footer, was written or edited since the label was last removed (ever, +// when it never was). A lane adding a label the PR already carries writes +// no `labeled` event, so its hold can sit under the App's label, and a +// person removing the label is what lifts it. // - Every escalation comment since that label event is the App's: its author // is the bot login and a Bot account, its first line is the pr-unblock // escalation marker, its second the resume-key line, and its `- Reason:` @@ -75,6 +81,11 @@ const PR_MERGE_MARKERS = Object.freeze([ "", ]); const AT_HEAD = / at head `([^`]*)`/g; +// A review lane's hold comment opens a line with this marker. The lanes post +// as claude[bot], or as github-actions[bot] with the lane's footer as the last +// non-blank line (address-feedback.mjs's lane comment rule). +const REVIEW_HOLD = ""; +const laneHold = (user, at, { footer = true, updatedAt = at } = {}) => + commented(user, `${HOLD}\nChanges a trust gate.${footer ? "\n\n`claude-lane: claude-security-review`\n" : ""}`, at, updatedAt); +const unlabeled = (actor, at) => ({ event: "unlabeled", actor, label: { name: "needs-human" }, created_at: at }); + +test("a review-lane hold keeps the label a clearing pr-unblock escalation would take off", async () => { + for (const hold of [laneHold(ACTIONS_BOT, "2026-10-18T11:00:00Z"), laneHold(CLAUDE_BOT, "2026-10-18T11:00:00Z")]) { + const w = world({ timeline: [labeled(BOT, "2026-10-18T10:00:00Z"), escalated(), hold] }); + const { logs } = await run(w); + assert.deepEqual(w.writes(), [], hold.user.login); + assert.match(logs.join("\n"), /#7 kept: a review lane holds it/, hold.user.login); + } +}); + +test("a review-lane hold written before the App's label, with no unlabel since, keeps it", async () => { + const w = world({ timeline: [laneHold(ACTIONS_BOT, "2026-10-18T09:00:00Z"), labeled(BOT, "2026-10-18T10:00:00Z"), escalated()] }); + await run(w); + assert.deepEqual(w.writes(), []); +}); + +test("a review-lane hold keeps a pr-merge head-moved escalation too", async () => { + const w = world({ timeline: [...mergeEscalated(verifyBody(OLD)), laneHold(ACTIONS_BOT, "2026-10-18T11:00:00Z")] }); + await run(w); + assert.deepEqual(w.writes(), []); +}); + +test("a review-lane hold a person approved by unlabeling no longer keeps a later escalation", async () => { + const w = world({ + timeline: [ + laneHold(ACTIONS_BOT, "2026-10-18T08:00:00Z"), + unlabeled(OWNER, "2026-10-18T09:00:00Z"), + labeled(BOT, "2026-10-18T10:00:00Z"), + escalated(), + ], + }); + await run(w); + assert.equal(w.writes().at(-1)?.method, "DELETE"); +}); + +test("a review-lane hold edited after the unlabel keeps it", async () => { + const w = world({ + timeline: [ + laneHold(ACTIONS_BOT, "2026-10-18T08:00:00Z", { updatedAt: "2026-10-18T09:30:00Z" }), + unlabeled(OWNER, "2026-10-18T09:00:00Z"), + labeled(BOT, "2026-10-18T10:00:00Z"), + escalated(), + ], + }); + await run(w); + assert.deepEqual(w.writes(), []); +}); + +test("a hold marker from github-actions[bot] with no lane footer, or from another bot, is not a review-lane hold", async () => { + for (const hold of [laneHold(ACTIONS_BOT, "2026-10-18T11:00:00Z", { footer: false }), laneHold(CODEX_BOT, "2026-10-18T11:00:00Z")]) { + const w = world({ timeline: [labeled(BOT, "2026-10-18T10:00:00Z"), escalated(), hold] }); + await run(w); + assert.equal(w.writes().at(-1)?.method, "DELETE", hold.user.login); + } +});