@@ -3,15 +3,21 @@ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
33import { tmpdir } from 'node:os' ;
44import { join } from 'node:path' ;
55import { execFileSync } from 'node:child_process' ;
6- import { request as httpRequest } from 'node:http' ;
6+ import { request as httpRequest , type IncomingMessage } from 'node:http' ;
7+ import { randomUUID } from 'node:crypto' ;
78import { createDemo } from '../../scripts/demo.ts' ;
89import { choiceKeys } from '../../core/approvals.ts' ;
910import { ReviewService } from '../../runner/review.ts' ;
1011import { startServer } from '../../web/server.ts' ;
1112import type { MergeGateway , MergeQueueGateway } from '../../github/merge.ts' ;
1213let root : string , app : Awaited < ReturnType < typeof startServer > > ;
13- // Resolves once the server has parsed a request's headers and is reading its body, so a partial request is provably admitted before shutdown starts.
14- function requestAdmitted ( server : typeof app . server ) { return new Promise < void > ( resolve => server . once ( 'request' , ( ) => resolve ( ) ) ) ; }
14+ // Resolves once the server has parsed the headers of the request that carries `marker` and is reading its body, so that partial request is provably admitted before shutdown starts.
15+ // Matching the marker matters: any other client on the port (a polling tab, a reused ephemeral port) also emits 'request' and would start shutdown too early.
16+ function requestAdmitted ( server : typeof app . server ) {
17+ const id = randomUUID ( ) , marker = { 'x-codeboost-test-request' :id } ;
18+ const admitted = new Promise < void > ( resolve => { const onRequest = ( req :IncomingMessage ) => { if ( req . headers [ 'x-codeboost-test-request' ] !== id ) return ; server . off ( 'request' , onRequest ) ; resolve ( ) ; } ; server . on ( 'request' , onRequest ) ; } ) ;
19+ return { admitted, marker} ;
20+ }
1521function removeDemoOutOfScope ( config : typeof app . service . config ) { chmodSync ( join ( config . repository , 'run.sh' ) , 0o644 ) ; execFileSync ( 'git' , [ '-c' , 'core.hooksPath=/dev/null' , 'commit' , '-am' , 'Restore declared scope' ] , { cwd :config . repository , stdio :'pipe' } ) ; }
1622test . beforeEach ( async ( ) => { root = mkdtempSync ( join ( tmpdir ( ) , 'codeboost-browser-' ) ) ; app = await startServer ( createDemo ( join ( root , 'demo' ) ) , 0 ) ; } ) ;
1723test . afterEach ( async ( ) => { await app . close ( ) ; rmSync ( root , { recursive :true , force :true } ) ; } ) ;
@@ -435,9 +441,9 @@ test('drains an in-flight question request before closing its agent manager',asy
435441 app = await startServer ( config , 0 , ( _prompt , signal ) => { calls ++ ; return new Promise ( ( _ , reject ) => signal . addEventListener ( 'abort' , ( ) => reject ( signal . reason ) , { once :true } ) ) ; } ) ;
436442 const view = app . service . load ( ) , body = JSON . stringify ( { action :'note' , item :'P1' , kind :'question' , text :'Question during shutdown' , token :view . token } ) ;
437443 const endpoint = new URL ( '/api/action' , app . url ) ;
438- let response = '' , finish ! :( ) => void ; const admitted = requestAdmitted ( app . server ) ;
444+ let response = '' , finish ! :( ) => void ; const { admitted, marker } = requestAdmitted ( app . server ) ;
439445 const completed = new Promise < void > ( ( resolve , reject ) => {
440- const req = httpRequest ( endpoint , { method :'POST' , headers :{ 'x-codeboost-token' :app . token , 'content-type' :'application/json' , 'content-length' :Buffer . byteLength ( body ) } } , res => {
446+ const req = httpRequest ( endpoint , { method :'POST' , headers :{ ... marker , 'x-codeboost-token' :app . token , 'content-type' :'application/json' , 'content-length' :Buffer . byteLength ( body ) } } , res => {
441447 res . setEncoding ( 'utf8' ) ; res . on ( 'data' , chunk => response += chunk ) ; res . on ( 'end' , resolve ) ;
442448 } ) ;
443449 req . on ( 'error' , reject ) ; req . write ( body . slice ( 0 , 1 ) ) ;
@@ -475,9 +481,9 @@ test('aborts an admitted review status inspection after the shutdown drain',asyn
475481} ) ;
476482test ( 'destroys a partial request body after the shutdown drain' , async ( ) => {
477483 const config = app . service . config ; await app . close ( ) ; app = await startServer ( config , 0 , undefined , undefined , 50 ) ;
478- const endpoint = new URL ( '/api/action' , app . url ) , body = JSON . stringify ( { action :'note' } ) , admitted = requestAdmitted ( app . server ) ;
484+ const endpoint = new URL ( '/api/action' , app . url ) , body = JSON . stringify ( { action :'note' } ) , { admitted, marker } = requestAdmitted ( app . server ) ;
479485 const completed = new Promise < 'response' | 'error' > ( resolve => {
480- const req = httpRequest ( endpoint , { method :'POST' , headers :{ 'x-codeboost-token' :app . token , 'content-type' :'application/json' , 'content-length' :Buffer . byteLength ( body ) } } , res => { res . resume ( ) ; res . on ( 'end' , ( ) => resolve ( 'response' ) ) ; } ) ;
486+ const req = httpRequest ( endpoint , { method :'POST' , headers :{ ... marker , 'x-codeboost-token' :app . token , 'content-type' :'application/json' , 'content-length' :Buffer . byteLength ( body ) } } , res => { res . resume ( ) ; res . on ( 'end' , ( ) => resolve ( 'response' ) ) ; } ) ;
481487 req . on ( 'error' , ( ) => resolve ( 'error' ) ) ; req . write ( body . slice ( 0 , 1 ) ) ;
482488 } ) ;
483489 await admitted ; const started = Date . now ( ) ; await app . close ( ) ; expect ( Date . now ( ) - started ) . toBeLessThan ( 1000 ) ; expect ( await completed ) . toBe ( 'error' ) ; app = await startServer ( config , 0 ) ;
@@ -488,8 +494,10 @@ test('blocks a partially received merge request when shutdown starts',async()=>{
488494 app = appRef = await startServer ( config , 0 , undefined , gateway ) ; let view = app . service . load ( ) ;
489495 for ( const segment of view . segments . filter ( value => value . row === 'Unplanned' || value . row === 'Ambiguous' ) ) view = app . service . act ( { action :'accept' , key :segment . key , token :view . token } ) ;
490496 for ( const item of view . items ) view = app . service . act ( { action :'approve' , item :item . id , confirmNoChange :item . count === 0 , token :view . token } ) ;
491- const body = JSON . stringify ( { action :'merge' , token :view . token } ) , endpoint = new URL ( '/api/action' , app . url ) ; let status = 0 , response = '' , finish ! :( ) => void ; const admitted = requestAdmitted ( app . server ) ;
492- const completed = new Promise < void > ( ( resolve , reject ) => { const req = httpRequest ( endpoint , { method :'POST' , headers :{ 'x-codeboost-token' :app . token , 'content-type' :'application/json' , 'content-length' :Buffer . byteLength ( body ) } } , res => { status = res . statusCode ?? 0 ; res . setEncoding ( 'utf8' ) ; res . on ( 'data' , chunk => response += chunk ) ; res . on ( 'end' , resolve ) ; } ) ; req . on ( 'error' , reject ) ; req . write ( body . slice ( 0 , 1 ) ) ; finish = ( ) => req . end ( body . slice ( 1 ) ) ; } ) ;
497+ const body = JSON . stringify ( { action :'merge' , token :view . token } ) , endpoint = new URL ( '/api/action' , app . url ) ; let status = 0 , response = '' , finish ! :( ) => void ; const { admitted, marker} = requestAdmitted ( app . server ) ;
498+ // Another client, such as a polling tab, reaches the server first; shutdown must still wait for the merge request itself.
499+ expect ( ( await fetch ( new URL ( '/api/settings' , app . url ) , { headers :{ 'x-codeboost-token' :app . token } } ) ) . status ) . toBe ( 200 ) ;
500+ const completed = new Promise < void > ( ( resolve , reject ) => { const req = httpRequest ( endpoint , { method :'POST' , headers :{ ...marker , 'x-codeboost-token' :app . token , 'content-type' :'application/json' , 'content-length' :Buffer . byteLength ( body ) } } , res => { status = res . statusCode ?? 0 ; res . setEncoding ( 'utf8' ) ; res . on ( 'data' , chunk => response += chunk ) ; res . on ( 'end' , resolve ) ; } ) ; req . on ( 'error' , reject ) ; req . write ( body . slice ( 0 , 1 ) ) ; finish = ( ) => req . end ( body . slice ( 1 ) ) ; } ) ;
493501 await Promise . race ( [ admitted , completed ] ) ; const closing = app . close ( ) ; finish ( ) ; await Promise . all ( [ closing , completed ] ) ;
494502 expect ( status ) . toBe ( 503 ) ; expect ( JSON . parse ( response ) . error ) . toMatch ( / s h u t t i n g d o w n / i) ; expect ( mergeCalls ) . toBe ( 0 ) ; app = await startServer ( config , 0 ) ;
495503} ) ;
0 commit comments