@@ -12,6 +12,9 @@ const OLD = '175328d0c1b2a3d4e5f60718293a4b5c6d7e8f90'
1212const HOST_FILE = 'core/src/host/turns.js' // T2: needs a PASS
1313const TEST_FILE = 'core/test/enqueue-guard.test.js' // T1: needs none
1414
15+ // #943: a pull request that may enter the queue says who owns it and who reviews it, in its body.
16+ const HEADERS = 'Owner: Dev\nReviewer: reviewer-agent\n'
17+
1518const policyFile = ( ) => {
1619 const path = join ( mkdtempSync ( join ( tmpdir ( ) , 'enqueue-guard-policy-' ) ) , 'review-policy.json' )
1720 writeFileSync ( path , JSON . stringify ( { reviewers : [ 'reviewer-agent' , 'Birfy' ] , selfReview : [ 'Birfy' ] , writers : [ 'reviewer-agent' , 'Reviewer' , 'Birfy' ] } ) )
@@ -29,7 +32,7 @@ const report = ({ tip = HEAD, verdict = 'PASS', extra = [] } = {}) =>
2932const comment = ( body = report ( ) , at = '2026-10-21T10:31:00Z' ) =>
3033 ( { user : { login : 'reviewer-agent' } , body, created_at : at , author_association : 'MEMBER' } )
3134
32- const fakeGh = ( { body = 'Fixes #602\n' , comments = [ ] , reviews = [ ] , labels = [ 'agent-pr' ] , files = [ ] , head = HEAD , author = 'opener' , headRepo = 'ThinkFlowLab/FlyBot' , permission = undefined } = { } ) => {
35+ const fakeGh = ( { body = HEADERS + 'Fixes #602\n' , comments = [ ] , reviews = [ ] , labels = [ 'agent-pr' ] , files = [ ] , head = HEAD , author = 'opener' , headRepo = 'ThinkFlowLab/FlyBot' , permission = undefined , draft = false , merged = false } = { } ) => {
3336 const calls = [ ]
3437 const gh = ( ...args ) => {
3538 const path = args [ args . length - 1 ]
@@ -41,7 +44,7 @@ const fakeGh = ({ body = 'Fixes #602\n', comments = [], reviews = [], labels = [
4144 if ( path . includes ( '/files' ) ) return files . map ( ( f ) => JSON . stringify ( typeof f === 'string' ? { filename : f } : f ) ) . join ( '\n' )
4245 if ( path . includes ( '/comments' ) ) return comments . map ( ( c ) => JSON . stringify ( c ) ) . join ( '\n' )
4346 if ( path . includes ( '/reviews' ) ) return reviews . map ( ( r ) => JSON . stringify ( r ) ) . join ( '\n' )
44- if ( / \/ p u l l s \/ \d + $ / . test ( path ) ) return JSON . stringify ( { head : { sha : head , repo : { full_name : headRepo } } , user : { login : author } , body, labels : labels . map ( ( name ) => ( { name } ) ) } )
47+ if ( / \/ p u l l s \/ \d + $ / . test ( path ) ) return JSON . stringify ( { head : { sha : head , repo : { full_name : headRepo } } , user : { login : author } , body, labels : labels . map ( ( name ) => ( { name } ) ) , draft , merged } )
4548 // Anything else — `gh pr merge` above all — is the guard doing more than checking.
4649 throw new Error ( 'fake gh: unexpected ' + path )
4750 }
@@ -190,7 +193,7 @@ test('#718: a pull request without the agent-pr label is not enqueued', () => {
190193} )
191194
192195test ( '#718: a Closes reference and the agent-pr label let it through' , ( ) => {
193- const gh = fakeGh ( { body : 'Closes #714\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
196+ const gh = fakeGh ( { body : HEADERS + 'Closes #714\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
194197 const r = main ( { gh, env : { REVIEW_POLICY : POLICY , GH_REPO : 'ThinkFlowLab/FlyBot' , PR : '566' } , repo : 'ThinkFlowLab/FlyBot' , pr : 566 } )
195198 assert . equal ( r . ok , true , r . reasons . join ( '; ' ) )
196199 assert . equal ( r . command , `gh pr merge 566 --auto --squash --match-head-commit ${ HEAD } ` )
@@ -200,21 +203,21 @@ test('#718: a Closes reference and the agent-pr label let it through', () => {
200203// `Fixes ThinkFlowLab/FlyBot#801`. The bare spelling there points at a number the fork does not have, so
201204// the guard has to accept the qualified one or every fork pull request is refused at the last step.
202205test ( 'a cross-repository Fixes reference is an issue reference' , ( ) => {
203- const gh = fakeGh ( { body : 'Fixes ThinkFlowLab/FlyBot#801\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
206+ const gh = fakeGh ( { body : HEADERS + 'Fixes ThinkFlowLab/FlyBot#801\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
204207 const r = main ( { gh, env : { REVIEW_POLICY : POLICY , GH_REPO : 'ThinkFlowLab/FlyBot' , PR : '566' } , repo : 'ThinkFlowLab/FlyBot' , pr : 566 } )
205208 assert . equal ( r . ok , true , r . reasons . join ( '; ' ) )
206209} )
207210
208211test ( 'the bare Fixes #<n> spelling still counts next to the cross-repository one' , ( ) => {
209- const gh = fakeGh ( { body : 'Fixes #801\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
212+ const gh = fakeGh ( { body : HEADERS + 'Fixes #801\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
210213 const r = main ( { gh, env : { REVIEW_POLICY : POLICY , GH_REPO : 'ThinkFlowLab/FlyBot' , PR : '566' } , repo : 'ThinkFlowLab/FlyBot' , pr : 566 } )
211214 assert . equal ( r . ok , true , r . reasons . join ( '; ' ) )
212215} )
213216
214217// The space after the keyword was optional before the cross-repository form existed and stays optional:
215218// widening the pattern must not quietly drop a spelling that used to count.
216219test ( 'a Fixes reference with no space before the hash still counts' , ( ) => {
217- const gh = fakeGh ( { body : 'Fixes#801\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
220+ const gh = fakeGh ( { body : HEADERS + 'Fixes#801\n\nthe detail' , labels : [ 'agent-pr' ] , files : [ HOST_FILE ] , comments : [ comment ( ) ] } )
218221 const r = main ( { gh, env : { REVIEW_POLICY : POLICY , GH_REPO : 'ThinkFlowLab/FlyBot' , PR : '566' } , repo : 'ThinkFlowLab/FlyBot' , pr : 566 } )
219222 assert . equal ( r . ok , true , r . reasons . join ( '; ' ) )
220223} )
@@ -327,3 +330,94 @@ test('#925: a selfReview login with admin on someone else\'s pull request is acc
327330 assert . equal ( r . verdict . by , 'Birfy' )
328331 assert . ok ( gh . calls . includes ( 'repos/ThinkFlowLab/FlyBot/collaborators/Birfy/permission' ) )
329332} )
333+
334+ // #943, part 1: the two lines the pipeline has been described with everywhere, read before the queue.
335+ // #940 went from opened to merged in about seven minutes with the largest diff of the day and its body
336+ // carried neither line, so the convention was a sentence nothing checked. Both lines present is not
337+ // enough on its own — the Reviewer named has to be someone who may write a conclusion at all.
338+ test ( '#943: a body with no Owner: line is refused, and the refusal names that line' , ( ) => {
339+ const r = run ( { body : 'Reviewer: reviewer-agent\nFixes #943\n' , comments : [ comment ( ) ] } )
340+ assert . equal ( r . ok , false )
341+ assert . ok ( r . reasons . includes ( 'missing line: Owner:' ) , r . reasons . join ( '; ' ) )
342+ assert . equal ( r . reasons . some ( ( reason ) => / m i s s i n g l i n e : R e v i e w e r : / . test ( reason ) ) , false , 'only the line that is missing is named' )
343+ } )
344+
345+ test ( '#943: a body with no Reviewer: line is refused, and the refusal names that line' , ( ) => {
346+ const r = run ( { body : 'Owner: Dev\nFixes #943\n' , comments : [ comment ( ) ] } )
347+ assert . equal ( r . ok , false )
348+ assert . ok ( r . reasons . includes ( 'missing line: Reviewer:' ) , r . reasons . join ( '; ' ) )
349+ assert . equal ( r . reasons . some ( ( reason ) => / m i s s i n g l i n e : O w n e r : / . test ( reason ) ) , false , 'only the line that is missing is named' )
350+ } )
351+
352+ test ( '#943: a Reviewer: naming a login outside `reviewers` is refused, and the refusal names the login' , ( ) => {
353+ const r = run ( { body : 'Owner: Dev\nReviewer: someone-else\nFixes #943\n' , comments : [ comment ( ) ] } )
354+ assert . equal ( r . ok , false )
355+ assert . ok ( r . reasons . includes ( 'Reviewer names "someone-else", who is not in reviewers' ) , r . reasons . join ( '; ' ) )
356+ } )
357+
358+ test ( '#943: an @ and any case are fine on a login `reviewers` lists' , ( ) => {
359+ const r = run ( { body : 'Owner: Dev\nReviewer: @Reviewer-Agent, BIRFY\nFixes #943\n' , comments : [ comment ( ) ] } )
360+ assert . equal ( r . ok , true , r . reasons . join ( '; ' ) )
361+ } )
362+
363+ // #941's shape: the body starts with the agent's own HTML comment, so the lines sit on the 4th and 5th
364+ // line, not on the first two. Reading only the first two would refuse every agent pull request.
365+ test ( '#943: the lines are found in the first 20 lines, not only the first two' , ( ) => {
366+ const r = run ( { body : '<!-- CURSOR_AGENT_PR_BODY_BEGIN -->\n\n' + HEADERS + 'Fixes #943\n' , comments : [ comment ( ) ] } )
367+ assert . equal ( r . ok , true , r . reasons . join ( '; ' ) )
368+ } )
369+
370+ test ( '#943: lines after the first 20 do not count' , ( ) => {
371+ const far = 'Fixes #943\n' + 'filler\n' . repeat ( 19 ) + HEADERS
372+ const r = run ( { body : far , comments : [ comment ( ) ] } )
373+ assert . equal ( r . ok , false )
374+ assert . ok ( r . reasons . includes ( 'missing line: Owner:' ) , r . reasons . join ( '; ' ) )
375+ assert . ok ( r . reasons . includes ( 'missing line: Reviewer:' ) , r . reasons . join ( '; ' ) )
376+ } )
377+
378+ // A draft is not entering the queue, and history is never re-read: the check runs on the enqueue request
379+ // for a live head only, so #940's merged record stays exactly as it is.
380+ test ( '#943: a draft is not refused by the body check' , ( ) => {
381+ const r = run ( { body : 'Fixes #943\n' , draft : true , comments : [ comment ( ) ] } )
382+ assert . equal ( r . ok , true , r . reasons . join ( '; ' ) )
383+ } )
384+
385+ // #943, part 2: a GitHub bot account writes no conclusion. The shared account every Bot writes as
386+ // (`Birfy`) is a `User` and keeps counting, which is why this is a rule about account type and not a
387+ // login list — a list containing `Birfy` would void the whole pipeline.
388+ test ( '#943: a Verdict: written by a bot account is not a conclusion here either' , ( ) => {
389+ const bot = { user : { login : 'cursor[bot]' , type : 'Bot' } , body : report ( ) , created_at : '2026-10-24T07:40:42Z' , author_association : 'NONE' }
390+ const shared = { user : { login : 'Birfy' , type : 'User' } , body : report ( ) . replace ( 'reviewer-agent' , 'Birfy' ) , created_at : '2026-10-24T07:45:10Z' , author_association : 'MEMBER' }
391+
392+ const alone = run ( { comments : [ bot ] } )
393+ assert . equal ( alone . ok , false )
394+ assert . match ( alone . reasons . join ( '; ' ) , / c u r s o r \[ b o t \] .* b o t a c c o u n t / )
395+ assert . equal ( alone . verdict . judged . find ( ( e ) => e . login === 'cursor[bot]' ) . counts , false )
396+
397+ const both = run ( { comments : [ bot , shared ] } )
398+ assert . equal ( both . ok , true , both . reasons . join ( '; ' ) )
399+ assert . equal ( both . verdict . by , 'Birfy' )
400+ } )
401+
402+ test ( '#943: a Reviewer: line with no login names nobody, so it is the missing line' , ( ) => {
403+ const r = run ( { body : 'Owner: Dev\nReviewer:\nFixes #943\n' , comments : [ comment ( ) ] } )
404+ assert . equal ( r . ok , false )
405+ assert . ok ( r . reasons . includes ( 'missing line: Reviewer:' ) , r . reasons . join ( '; ' ) )
406+ } )
407+
408+ // #943 INC at 87b49961: this check runs again on every comment (enqueue-guard.yml, `issue_comment`),
409+ // so "a merged pull request is never re-judged" was only true of the code that never read `merged`.
410+ // A sentence on #926/#925/#908 — merged without these lines, their bodies uneditable now — turned
411+ // their check red. A merged pull request is history; only a live request is judged.
412+ test ( '#943 INC: a merged pull request is not re-judged when a comment arrives' , ( ) => {
413+ const body = 'Fixes #602\n\nthe detail' // no Owner:/Reviewer:, the shape #926/#925/#908 merged with
414+ const merged = run ( { body, merged : true , comments : [ comment ( ) ] } )
415+ assert . equal ( merged . ok , true , merged . reasons . join ( '; ' ) )
416+ assert . equal ( merged . reasons . some ( ( reason ) => / m i s s i n g l i n e : / . test ( reason ) ) , false )
417+ // The negative control: the same body on an open, non-draft pull request is still refused, so the
418+ // exemption is `merged` and not a missing fixture field.
419+ const live = run ( { body, comments : [ comment ( ) ] } )
420+ assert . equal ( live . ok , false )
421+ assert . ok ( live . reasons . includes ( 'missing line: Owner:' ) , live . reasons . join ( '; ' ) )
422+ assert . ok ( live . reasons . includes ( 'missing line: Reviewer:' ) , live . reasons . join ( '; ' ) )
423+ } )
0 commit comments