@@ -195,33 +195,52 @@ describe("repo command authorization policy", () => {
195195 it ( "#5084: a chat pr_author match is only granted when commandRateLimitPolicy is \"hold\" for the repo" , ( ) => {
196196 // No rate-limit policy passed at all (the undefined branch) -- denied, with a distinct reason from the
197197 // generic denials so an operator can tell "rate limiting isn't on" apart from "not authorized at all".
198+ // pullRequestOpenAndNotDraft: true throughout, so this test isolates the rate-limit gate specifically.
198199 expect (
199- evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "author" , pullRequestAuthorLogin : "author" } ) ,
200+ evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "author" , pullRequestAuthorLogin : "author" , pullRequestOpenAndNotDraft : true } ) ,
200201 ) . toMatchObject ( { authorized : false , reason : "pr_author_requires_rate_limiting" , actorKind : "author" , matchedRole : null } ) ;
201202 // Explicitly "off" (not just unset) -- same denial, covering both falsy branches of the `!== "hold"` check.
202203 expect (
203- evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "author" , pullRequestAuthorLogin : "author" , commandRateLimitPolicy : "off" } ) ,
204+ evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "author" , pullRequestAuthorLogin : "author" , commandRateLimitPolicy : "off" , pullRequestOpenAndNotDraft : true } ) ,
204205 ) . toMatchObject ( { authorized : false , reason : "pr_author_requires_rate_limiting" } ) ;
205206 // "hold" -- the PR's own author is authorized.
206207 expect (
207- evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "author" , pullRequestAuthorLogin : "author" , commandRateLimitPolicy : "hold" } ) ,
208+ evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "author" , pullRequestAuthorLogin : "author" , commandRateLimitPolicy : "hold" , pullRequestOpenAndNotDraft : true } ) ,
208209 ) . toMatchObject ( { authorized : true , reason : "allowed_pr_author" , actorKind : "author" , matchedRole : "pr_author" } ) ;
209210 // A confirmed miner acting on their OWN PR matches pr_author first (chat's roles list has pr_author, not
210211 // confirmed_miner) -- so a miner is gated by the SAME rate-limit requirement as any other PR author, not
211212 // the separate confirmed_miner exception "review" gets.
212213 expect (
213- evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "miner" , pullRequestAuthorLogin : "miner" , minerStatus : "confirmed" } ) ,
214+ evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "miner" , pullRequestAuthorLogin : "miner" , minerStatus : "confirmed" , pullRequestOpenAndNotDraft : true } ) ,
214215 ) . toMatchObject ( { authorized : false , reason : "pr_author_requires_rate_limiting" } ) ;
215216 expect (
216- evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "miner" , pullRequestAuthorLogin : "miner" , minerStatus : "confirmed" , commandRateLimitPolicy : "hold" } ) ,
217+ evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "miner" , pullRequestAuthorLogin : "miner" , minerStatus : "confirmed" , commandRateLimitPolicy : "hold" , pullRequestOpenAndNotDraft : true } ) ,
217218 ) . toMatchObject ( { authorized : true , reason : "allowed_pr_author" , matchedRole : "pr_author" } ) ;
218219 // A commenter on someone ELSE's PR is still denied outright -- pr_author never matches for a non-author,
219220 // rate limiting or not.
220221 expect (
221- evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "other" , pullRequestAuthorLogin : "author" , commandRateLimitPolicy : "hold" } ) ,
222+ evaluateCommandAuthorization ( { commandName : "chat" , commenterLogin : "other" , pullRequestAuthorLogin : "author" , commandRateLimitPolicy : "hold" , pullRequestOpenAndNotDraft : true } ) ,
222223 ) . toMatchObject ( { authorized : false , reason : "not_maintainer_or_pr_author" } ) ;
223224 } ) ;
224225
226+ it ( "#5092: a chat pr_author match is ALSO only granted when the PR is open and not draft" , ( ) => {
227+ // commandRateLimitPolicy: "hold" throughout, so this test isolates the PR-state gate specifically. The
228+ // per-PR rate-limit counter (repoFullName#issueNumber#command) never checks PR state on its own, so
229+ // without this a contributor could keep a fresh chat allowance forever by reopening/reusing a closed PR
230+ // or spamming cheap draft PRs.
231+ const base = { commandName : "chat" , commenterLogin : "author" , pullRequestAuthorLogin : "author" , commandRateLimitPolicy : "hold" as const } ;
232+ // Unset (the undefined branch) -- denied, distinct reason from the rate-limit denial.
233+ expect ( evaluateCommandAuthorization ( base ) ) . toMatchObject ( { authorized : false , reason : "pr_author_requires_open_pr" , actorKind : "author" , matchedRole : null } ) ;
234+ // Explicitly false (not just unset) -- same denial, covering both falsy branches of the `!== true` check.
235+ expect ( evaluateCommandAuthorization ( { ...base , pullRequestOpenAndNotDraft : false } ) ) . toMatchObject ( { authorized : false , reason : "pr_author_requires_open_pr" } ) ;
236+ // Open and not draft -- authorized.
237+ expect ( evaluateCommandAuthorization ( { ...base , pullRequestOpenAndNotDraft : true } ) ) . toMatchObject ( { authorized : true , reason : "allowed_pr_author" , matchedRole : "pr_author" } ) ;
238+ // Maintainers/collaborators are completely unaffected by PR state -- the check only bounds the
239+ // less-trusted pr_author tier, never already-trusted roles.
240+ expect ( evaluateCommandAuthorization ( { commandName : "chat" , commenterAssociation : "OWNER" } ) ) . toMatchObject ( { authorized : true , reason : "maintainer_invocation" } ) ;
241+ expect ( evaluateCommandAuthorization ( { commandName : "chat" , commenterAssociation : "COLLABORATOR" } ) ) . toMatchObject ( { authorized : true , reason : "collaborator_invocation" } ) ;
242+ } ) ;
243+
225244 it ( "#5084: a maintainer's yml override restating chat's own default (incl. pr_author) is not clamped away" , ( ) => {
226245 const restated = normalizeCommandAuthorizationPolicy ( { commands : { chat : [ "collaborator" , "pr_author" ] } } ) ;
227246 expect ( restated . warnings ) . not . toContain ( "Ignored author command authorization roles for maintainer-only command: chat." ) ;
0 commit comments