@@ -231,6 +231,7 @@ await withMockedModule(
231231const {
232232 connectMCPServer,
233233 resetBrowserAuthState,
234+ setBrowserAuthWaitMs,
234235 MAX_BROWSER_AUTH_ATTEMPTS ,
235236 BROWSER_AUTH_COOLDOWN_MS ,
236237} = await import ( "./client.js" ) ;
@@ -452,13 +453,11 @@ describe("HTTP MCP re-auth loop prevention", () => {
452453 expect ( connected . ok ) . toBe ( true ) ;
453454 if ( ! connected . ok ) return ;
454455
455- for ( let episode = 0 ; episode < 2 ; episode += 1 ) {
456- callFailuresLeft = 1 ;
457- await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
458- "finishAuth exploded" ,
459- ) ;
460- }
461- expect ( authURLCount ) . toBe ( 2 ) ;
456+ callFailuresLeft = 1 ;
457+ await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
458+ "finishAuth exploded" ,
459+ ) ;
460+ expect ( authURLCount ) . toBe ( 1 ) ;
462461 expect ( authorizedCount ) . toBe ( 0 ) ;
463462
464463 refreshSucceeds = true ;
@@ -467,7 +466,7 @@ describe("HTTP MCP re-auth loop prevention", () => {
467466 callFailuresLeft = 1 ;
468467 await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . resolves . toBe ( "" ) ;
469468 expect ( authorizedCount ) . toBe ( 1 ) ;
470- expect ( authURLCount ) . toBe ( 2 ) ;
469+ expect ( authURLCount ) . toBe ( 1 ) ;
471470
472471 refreshSucceeds = false ;
473472 finishAuthError = new Error ( "finishAuth exploded" ) ;
@@ -478,13 +477,13 @@ describe("HTTP MCP re-auth loop prevention", () => {
478477 "finishAuth exploded" ,
479478 ) ;
480479 }
481- expect ( authURLCount ) . toBe ( 2 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
480+ expect ( authURLCount ) . toBe ( 1 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
482481
483482 callFailuresLeft = 1 ;
484483 await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
485484 "retrying paused" ,
486485 ) ;
487- expect ( authURLCount ) . toBe ( 2 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
486+ expect ( authURLCount ) . toBe ( 1 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
488487 } ) ;
489488
490489 test ( "client close aborts the shared callback waiter" , async ( ) => {
@@ -559,22 +558,20 @@ describe("HTTP MCP re-auth loop prevention", () => {
559558 const second = connected . client . call ( "second" , { } , new AbortController ( ) . signal ) ;
560559 while ( callToolCalls < 4 ) await Promise . resolve ( ) ;
561560
562- callbackGate = new Promise ( ( resolve ) => {
563- releaseCallback = resolve ;
564- } ) ;
561+ retryGate = undefined ;
562+ refreshSucceeds = true ;
563+ redirectsPerFailure = 0 ;
565564 callFailuresLeft = 1 ;
566565 const third = connected . client . call ( "third" , { } , new AbortController ( ) . signal ) ;
567- while ( waitForCodeCalls < 2 ) await Promise . resolve ( ) ;
566+ await expect ( third ) . resolves . toBe ( "" ) ;
567+ expect ( authorizedCount ) . toBe ( 1 ) ;
568+ expect ( authURLCount ) . toBe ( 1 ) ;
568569
569570 releaseRetry ?.( ) ;
570571 await expect ( first ) . resolves . toBe ( "" ) ;
571572 await expect ( second ) . resolves . toBe ( "" ) ;
572- expect ( authorizedCount ) . toBe ( 0 ) ;
573-
574- releaseCallback ?.( ) ;
575- await expect ( third ) . resolves . toBe ( "" ) ;
576573 expect ( authorizedCount ) . toBe ( 1 ) ;
577- expect ( authURLCount ) . toBe ( 2 ) ;
574+ expect ( waitForCodeCalls ) . toBe ( 1 ) ;
578575 } ) ;
579576
580577 test ( "does not repeat the SDK refresh after redirecting to authorization" , async ( ) => {
@@ -590,7 +587,29 @@ describe("HTTP MCP re-auth loop prevention", () => {
590587 expect ( authURLCount ) . toBe ( 1 ) ;
591588 } ) ;
592589
593- test ( "live-call auth episodes share redirect state and stop after three prompts" , async ( ) => {
590+ test ( "reconnect during in-flight waitForCode does not emit a second prompt" , async ( ) => {
591+ connectFailuresLeft = Number . POSITIVE_INFINITY ;
592+ callbackGate = new Promise ( ( ) => undefined ) ;
593+ const firstAbort = new AbortController ( ) ;
594+ const first = connectMCPServer ( config , {
595+ onAuthURL : ( ) => ( authURLCount += 1 ) ,
596+ signal : firstAbort . signal ,
597+ } ) ;
598+ while ( authURLCount === 0 || waitForCodeCalls === 0 ) await Promise . resolve ( ) ;
599+ expect ( authURLCount ) . toBe ( 1 ) ;
600+
601+ const second = await connectWithAuthPrompt ( ) ;
602+
603+ expect ( second . ok ) . toBe ( false ) ;
604+ expect ( second . error ) . toContain ( "retrying paused" ) ;
605+ expect ( authURLCount ) . toBe ( 1 ) ;
606+ expect ( waitForCodeCalls ) . toBe ( 1 ) ;
607+
608+ firstAbort . abort ( ) ;
609+ await expect ( first ) . resolves . toMatchObject ( { ok : false } ) ;
610+ } ) ;
611+
612+ test ( "live-call auth episodes share redirect state and stop after one prompt" , async ( ) => {
594613 const connected = await connectMCPServer ( config , {
595614 onAuthURL : ( ) => {
596615 authURLCount += 1 ;
@@ -650,7 +669,7 @@ describe("HTTP MCP re-auth loop prevention", () => {
650669 const result = await connectWithAuthPrompt ( ) ;
651670 expect ( result . ok ) . toBe ( false ) ;
652671 expect ( result . error ) . toContain (
653- `MCP authorization for linear failed after ${ MAX_BROWSER_AUTH_ATTEMPTS } attempts` ,
672+ `MCP authorization for linear failed after ${ MAX_BROWSER_AUTH_ATTEMPTS } ${ MAX_BROWSER_AUTH_ATTEMPTS === 1 ? "attempt" : " attempts" } ` ,
654673 ) ;
655674 expect ( result . error ) . toContain ( "retrying paused for 5 minutes" ) ;
656675 expect ( result . error ) . toContain ( "Retry later after the cooldown" ) ;
@@ -663,19 +682,11 @@ describe("HTTP MCP re-auth loop prevention", () => {
663682 } ) ;
664683
665684 test ( "successful interactive auth clears the cap so a later failure can prompt" , async ( ) => {
666- connectFailuresLeft = Number . POSITIVE_INFINITY ;
667- for ( let episode = 0 ; episode < 2 ; episode += 1 ) {
668- const result = await connectWithAuthPrompt ( ) ;
669- expect ( result . ok ) . toBe ( false ) ;
670- expect ( result . error ) . toContain ( "finishAuth exploded" ) ;
671- }
672- expect ( authURLCount ) . toBe ( 2 ) ;
673-
674685 finishAuthError = undefined ;
675686 connectFailuresLeft = 1 ;
676687 const recovered = await connectWithAuthPrompt ( ) ;
677688 expect ( recovered . ok ) . toBe ( true ) ;
678- expect ( authURLCount ) . toBe ( 3 ) ;
689+ expect ( authURLCount ) . toBe ( 1 ) ;
679690
680691 finishAuthError = new Error ( "finishAuth exploded" ) ;
681692 connectFailuresLeft = Number . POSITIVE_INFINITY ;
@@ -684,15 +695,15 @@ describe("HTTP MCP re-auth loop prevention", () => {
684695 expect ( result . ok ) . toBe ( false ) ;
685696 expect ( result . error ) . toContain ( "finishAuth exploded" ) ;
686697 }
687- expect ( authURLCount ) . toBe ( 3 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
698+ expect ( authURLCount ) . toBe ( 1 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
688699
689700 const capped = await connectWithAuthPrompt ( ) ;
690701 expect ( capped . ok ) . toBe ( false ) ;
691702 expect ( capped . error ) . toContain ( "retrying paused" ) ;
692- expect ( authURLCount ) . toBe ( 3 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
703+ expect ( authURLCount ) . toBe ( 1 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
693704 } ) ;
694705
695- test ( "prompts resume five minutes after the third failed episode" , async ( ) => {
706+ test ( "prompts resume five minutes after the capped failed episode" , async ( ) => {
696707 const thirdEpisodeAt = new Date ( "2026-01-01T00:00:00Z" ) . getTime ( ) ;
697708 setSystemTime ( thirdEpisodeAt ) ;
698709 connectFailuresLeft = Number . POSITIVE_INFINITY ;
@@ -847,20 +858,11 @@ describe("HTTP MCP re-auth loop prevention", () => {
847858
848859 finishAuthError = new Error ( "finishAuth exploded" ) ;
849860 callRedirectsLeft = Number . POSITIVE_INFINITY ;
850- for ( let episode = 0 ; episode < 2 ; episode += 1 ) {
851- callFailuresLeft = 1 ;
852- await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
853- "finishAuth exploded" ,
854- ) ;
855- }
856- expect ( authURLCount ) . toBe ( 3 ) ;
857- expect ( authorizedCount ) . toBe ( 0 ) ;
858-
859861 callFailuresLeft = 1 ;
860862 await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
861863 "retrying paused" ,
862864 ) ;
863- expect ( authURLCount ) . toBe ( 3 ) ;
865+ expect ( authURLCount ) . toBe ( 1 ) ;
864866 expect ( authorizedCount ) . toBe ( 0 ) ;
865867 } ) ;
866868
@@ -877,33 +879,46 @@ describe("HTTP MCP re-auth loop prevention", () => {
877879 expect ( connected . ok ) . toBe ( true ) ;
878880 if ( ! connected . ok ) return ;
879881
880- callFailuresLeft = 2 ;
881- callRedirectsLeft = 1 ;
882- await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
883- "authorization required" ,
884- ) ;
885- expect ( authorizedCount ) . toBe ( 0 ) ;
886-
887- callRedirectsLeft = Number . POSITIVE_INFINITY ;
888882 callFailuresLeft = 1 ;
883+ callRedirectsLeft = 1 ;
889884 await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . resolves . toBe ( "" ) ;
890885 expect ( authorizedCount ) . toBe ( 1 ) ;
891- expect ( authURLCount ) . toBe ( 2 ) ;
886+ expect ( authURLCount ) . toBe ( 1 ) ;
892887
893888 finishAuthError = new Error ( "finishAuth exploded" ) ;
889+ callRedirectsLeft = Number . POSITIVE_INFINITY ;
894890 for ( let episode = 0 ; episode < MAX_BROWSER_AUTH_ATTEMPTS ; episode += 1 ) {
895891 callFailuresLeft = 1 ;
896892 await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
897893 "finishAuth exploded" ,
898894 ) ;
899895 }
900- expect ( authURLCount ) . toBe ( 2 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
896+ expect ( authURLCount ) . toBe ( 1 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
901897
902898 callFailuresLeft = 1 ;
903899 await expect ( connected . client . call ( "ping" , { } , new AbortController ( ) . signal ) ) . rejects . toThrow (
904900 "retrying paused" ,
905901 ) ;
906- expect ( authURLCount ) . toBe ( 2 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
902+ expect ( authURLCount ) . toBe ( 1 + MAX_BROWSER_AUTH_ATTEMPTS ) ;
907903 expect ( authorizedCount ) . toBe ( 1 ) ;
908904 } ) ;
905+
906+ test ( "ignored browser wait times out as disconnected and still counts the prompt" , async ( ) => {
907+ setBrowserAuthWaitMs ( 50 ) ;
908+ connectFailuresLeft = Number . POSITIVE_INFINITY ;
909+ callbackGate = new Promise ( ( ) => undefined ) ;
910+
911+ const result = await connectWithAuthPrompt ( ) ;
912+
913+ expect ( result . ok ) . toBe ( false ) ;
914+ expect ( result . error ) . toContain ( "timed out waiting for the browser" ) ;
915+ expect ( result . error ) . toContain ( "disconnected" ) ;
916+ expect ( authURLCount ) . toBe ( 1 ) ;
917+ expect ( waitForCodeCalls ) . toBe ( 1 ) ;
918+
919+ const capped = await connectWithAuthPrompt ( ) ;
920+ expect ( capped . ok ) . toBe ( false ) ;
921+ expect ( capped . error ) . toContain ( "retrying paused" ) ;
922+ expect ( authURLCount ) . toBe ( 1 ) ;
923+ } ) ;
909924} ) ;
0 commit comments