@@ -171,6 +171,53 @@ describe('issue endpoints', { timeout: 30_000 }, () => {
171171 } finally { spy . mockRestore ( ) ; await app . close ( ) ; }
172172 } ) ;
173173
174+ it ( 'stops waiting when the response closed before the Issues listener was attached' , async ( ) => {
175+ root = mkdtempSync ( join ( tmpdir ( ) , 'codeboost-issues-' ) ) ;
176+ const { gateway, calls } = heldGateway ( ) ;
177+ const waits : Promise < unknown > [ ] = [ ] ;
178+ let closed = false ;
179+ let closedBeforeRefresh = false ;
180+ let disconnectNext = true ;
181+ const resumeHandler = deferred < void > ( ) ;
182+ const original = IssueBoard . prototype . refresh ;
183+ const spy = vi . spyOn ( IssueBoard . prototype , 'refresh' ) . mockImplementation ( function ( this : IssueBoard , signal ) {
184+ closedBeforeRefresh = closed ;
185+ const wait = original . call ( this , signal ) ;
186+ waits . push ( wait . then ( ( ) => 'settled' , error => String ( error ) ) ) ;
187+ return wait ;
188+ } ) ;
189+ const app = await startServer ( createDemo ( join ( root , 'demo' ) ) , 0 , undefined , undefined , undefined , gateway ,
190+ undefined , undefined , undefined , { beforeIssueRefreshWait : ( ) => resumeHandler . promise } ) ;
191+ app . server . prependListener ( 'request' , ( req , res ) => {
192+ if ( req . url !== '/api/issues' || ! disconnectNext ) return ;
193+ disconnectNext = false ;
194+ req . once ( 'end' , ( ) => {
195+ res . once ( 'close' , ( ) => { closed = true ; resumeHandler . resolve ( ) ; } ) ;
196+ res . destroy ( ) ;
197+ } ) ;
198+ } ) ;
199+ try {
200+ const leaving = call ( app . url , app . token , 'POST' , { action : 'refresh' } ) . catch ( ( ) => undefined ) ;
201+ await expect . poll ( ( ) => waits . length ) . toBe ( 1 ) ;
202+ expect ( closedBeforeRefresh ) . toBe ( true ) ;
203+ const staying = call ( app . url , app . token , 'POST' , { action : 'refresh' } ) ;
204+ await expect . poll ( ( ) => waits . length ) . toBe ( 2 ) ;
205+ let settled = false ;
206+ void waits [ 0 ] ! . then ( ( ) => { settled = true ; } ) ;
207+ await expect . poll ( ( ) => settled , { timeout : 500 } ) . toBe ( true ) ;
208+ expect ( await waits [ 0 ] ) . toContain ( 'Client disconnected' ) ;
209+ expect ( calls [ 0 ] ! . signal ! . aborted ) . toBe ( false ) ;
210+ calls [ 0 ] ! . result . resolve ( snapshot ( ) ) ;
211+ await expect ( staying ) . resolves . toMatchObject ( { status : 200 , body : { state : { state : 'fresh' } } } ) ;
212+ expect ( calls ) . toHaveLength ( 1 ) ;
213+ await leaving ;
214+ } finally {
215+ calls [ 0 ] ?. result . resolve ( snapshot ( ) ) ;
216+ spy . mockRestore ( ) ;
217+ await app . close ( ) ;
218+ }
219+ } ) ;
220+
174221 it ( 'shutdown aborts an admitted refresh and waits for the retrieval to settle' , async ( ) => {
175222 root = mkdtempSync ( join ( tmpdir ( ) , 'codeboost-issues-' ) ) ;
176223 const { gateway, calls } = heldGateway ( ) ;
0 commit comments