@@ -88,10 +88,11 @@ describe('initUpdater state machine', () => {
8888 }
8989
9090 /** Replays a native Squirrel.Mac event, e.g. `update-downloaded` once a bundle is staged. */
91- function emitSquirrel ( event : string ) {
91+ function emitSquirrel ( event : string , ...args : unknown [ ] ) {
92+ if ( event === 'error' ) emit ( event , ...args )
9293 for ( const [ name , listener ] of vi . mocked ( squirrelUpdater . on ) . mock . calls ) {
9394 if ( name === event ) {
94- ; ( listener as ( ) => void ) ( )
95+ ; ( listener as ( ... values : unknown [ ] ) => void ) ( ... args )
9596 }
9697 }
9798 }
@@ -274,7 +275,7 @@ describe('initUpdater state machine', () => {
274275 expect ( autoUpdaterMock . quitAndInstall ) . toHaveBeenCalledTimes ( 1 )
275276 } )
276277
277- it ( 'does not install when the updater fails during pre-install teardown' , async ( ) => {
278+ it ( 'does not install when native staging fails during teardown with a background check pending ' , async ( ) => {
278279 let finishTeardown : ( ( ) => void ) | undefined
279280 const setRelaunchPending = vi . fn ( )
280281 const { handle } = await createUpdater ( {
@@ -290,14 +291,15 @@ describe('initUpdater state machine', () => {
290291 emit ( 'update-available' , { version : '2.0.0' } )
291292 emit ( 'update-downloaded' , { version : '2.0.0' } )
292293 emitSquirrel ( 'update-downloaded' )
294+ await vi . advanceTimersByTimeAsync ( 10_000 )
293295 vi . mocked ( dialog . showMessageBox ) . mockResolvedValueOnce ( {
294296 response : 1 ,
295297 checkboxChecked : false ,
296298 } )
297299 handle . install ( )
298300 await vi . advanceTimersByTimeAsync ( 0 )
299301
300- emit ( 'error' , new Error ( 'native installer failed' ) )
302+ emitSquirrel ( 'error' , new Error ( 'native installer failed' ) )
301303 finishTeardown ?.( )
302304 await vi . advanceTimersByTimeAsync ( 0 )
303305
@@ -306,6 +308,39 @@ describe('initUpdater state machine', () => {
306308 expect ( autoUpdaterMock . quitAndInstall ) . not . toHaveBeenCalled ( )
307309 } )
308310
311+ it ( 'finishes a confirmed restart when an earlier background check fails during teardown' , async ( ) => {
312+ let finishTeardown : ( ( ) => void ) | undefined
313+ let failRefresh : ( ( error : Error ) => void ) | undefined
314+ const { handle } = await createUpdater ( {
315+ beforeInstall : ( ) =>
316+ new Promise < void > ( ( resolve ) => {
317+ finishTeardown = resolve
318+ } ) ,
319+ } )
320+ await stageUpdate ( handle , '2.0.0' )
321+ autoUpdaterMock . checkForUpdates . mockImplementationOnce (
322+ ( ) =>
323+ new Promise ( ( _ , reject ) => {
324+ failRefresh = ( error ) => {
325+ emit ( 'error' , error )
326+ reject ( error )
327+ }
328+ } )
329+ )
330+ await vi . advanceTimersByTimeAsync ( 10_000 )
331+ vi . mocked ( dialog . showMessageBox ) . mockResolvedValueOnce ( { response : 1 , checkboxChecked : false } )
332+ handle . install ( )
333+ await vi . advanceTimersByTimeAsync ( 0 )
334+
335+ failRefresh ?.( new Error ( 'Background feed unavailable' ) )
336+ await vi . advanceTimersByTimeAsync ( 0 )
337+ expect ( handle . getState ( ) ) . toEqual ( { status : 'ready' , version : '2.0.0' } )
338+ expect ( autoUpdaterMock . autoInstallOnAppQuit ) . toBe ( true )
339+ finishTeardown ?.( )
340+ await vi . advanceTimersByTimeAsync ( 0 )
341+ expect ( autoUpdaterMock . quitAndInstall ) . toHaveBeenCalledTimes ( 1 )
342+ } )
343+
309344 it ( 'bypasses renderer unload guards only after teardown succeeds' , async ( ) => {
310345 const setRelaunchPending = vi . fn ( )
311346 vi . mocked ( dialog . showMessageBox ) . mockResolvedValueOnce ( {
@@ -382,16 +417,17 @@ describe('initUpdater state machine', () => {
382417 emit ( 'update-available' , { version : '2.0.0' } )
383418 await vi . advanceTimersByTimeAsync ( 30 * 60 * 1000 - 10_000 )
384419 emit ( 'update-not-available' )
420+ autoUpdaterMock . checkForUpdates . mockRejectedValueOnce ( new Error ( 'net::ERR_NETWORK_CHANGED' ) )
385421 await vi . advanceTimersByTimeAsync ( 30 * 60 * 1000 )
386- emit ( 'error' , new Error ( 'net::ERR_NETWORK_CHANGED' ) )
387422 await vi . advanceTimersByTimeAsync ( 30 * 60 * 1000 )
423+ autoUpdaterMock . downloadUpdate . mockRejectedValueOnce ( new Error ( 'download interrupted' ) )
388424 emit ( 'update-available' , { version : '2.1.0' } )
389- emit ( 'error' , new Error ( 'download interrupted' ) )
425+ await vi . advanceTimersByTimeAsync ( 0 )
390426 expect ( handle . getState ( ) ) . toEqual ( { status : 'ready' , version : '2.0.0' } )
391427 await vi . advanceTimersByTimeAsync ( 30 * 60 * 1000 )
392428 emit ( 'update-available' , { version : '2.2.0' } )
393429 emit ( 'update-downloaded' , { version : '2.2.0' } )
394- emit ( 'error' , new Error ( 'Squirrel could not verify the replacement' ) )
430+ emitSquirrel ( 'error' , new Error ( 'Squirrel could not verify the replacement' ) )
395431
396432 expect ( autoUpdaterMock . checkForUpdates ) . toHaveBeenCalledTimes ( 6 )
397433 expect ( autoUpdaterMock . downloadUpdate ) . toHaveBeenCalledTimes ( 3 )
0 commit comments