@@ -886,7 +886,10 @@ describe("AuthService", () => {
886886 } ) ;
887887
888888 describe ( "project-less recovery" , ( ) => {
889- function stubOrgFetch ( state : { succeeds : boolean ; orgCalls : number } ) {
889+ function stubOrgFetch (
890+ state : { succeeds : boolean ; orgCalls : number } ,
891+ options : { orgless ?: boolean } = { } ,
892+ ) {
890893 vi . stubGlobal (
891894 "fetch" ,
892895 vi . fn ( async ( input : string | Request ) => {
@@ -897,7 +900,7 @@ describe("AuthService", () => {
897900 ok : true ,
898901 json : vi . fn ( ) . mockResolvedValue ( {
899902 uuid : "user-1" ,
900- organization : { id : "org-1" } ,
903+ organization : options . orgless ? null : { id : "org-1" } ,
901904 } ) ,
902905 } as unknown as Response ;
903906 }
@@ -1156,27 +1159,63 @@ describe("AuthService", () => {
11561159 expect ( sessionPort . getCurrent ( ) ?. selectedProjectId ) . toBe ( 84 ) ;
11571160 } ) ;
11581161
1159- it ( "does not attempt recovery when the token grants no scoped organizations" , async ( ) => {
1160- const fetchState = { succeeds : true , orgCalls : 0 } ;
1161- stubOrgFetch ( fetchState ) ;
1162- oauthFlow . startFlow . mockResolvedValue (
1163- mockTokenResponse ( { scopedOrgs : [ ] } ) ,
1164- ) ;
1165-
1166- await service . login ( "us" ) ;
1162+ // Team-scoped tokens (required_access_level=project) arrive with empty
1163+ // scoped_organizations on some backends. When @me carries a current org,
1164+ // we fetch it so the picker isn't stranded on "No projects". When @me has
1165+ // no org either (truly orgless user), there's nothing to fetch and the
1166+ // recovery loop must not spin — that was the concern from #2655.
1167+ it . each ( [
1168+ {
1169+ when : "the user has no organization at all" ,
1170+ orgless : true ,
1171+ expectedOrgCalls : 0 ,
1172+ expectedState : {
1173+ status : "authenticated" ,
1174+ currentProjectId : null ,
1175+ orgProjectsMap : { } ,
1176+ } ,
1177+ } ,
1178+ {
1179+ when : "the @me current org is used as a fallback" ,
1180+ orgless : false ,
1181+ expectedOrgCalls : 1 ,
1182+ expectedState : {
1183+ status : "authenticated" ,
1184+ currentOrgId : "org-1" ,
1185+ currentProjectId : 42 ,
1186+ orgProjectsMap : {
1187+ "org-1" : {
1188+ orgName : "Org 1" ,
1189+ projects : [
1190+ { id : 42 , name : "Project 42" } ,
1191+ { id : 84 , name : "Project 84" } ,
1192+ ] ,
1193+ } ,
1194+ } ,
1195+ } ,
1196+ } ,
1197+ ] ) (
1198+ "with empty scoped_organizations: $when" ,
1199+ async ( { orgless, expectedOrgCalls, expectedState } ) => {
1200+ const fetchState = { succeeds : true , orgCalls : 0 } ;
1201+ stubOrgFetch ( fetchState , { orgless } ) ;
1202+ oauthFlow . startFlow . mockResolvedValue (
1203+ mockTokenResponse ( { scopedOrgs : [ ] } ) ,
1204+ ) ;
11671205
1168- expect ( service . getState ( ) ) . toMatchObject ( {
1169- status : "authenticated" ,
1170- currentProjectId : null ,
1171- orgProjectsMap : { } ,
1172- } ) ;
1206+ await service . login ( "us" ) ;
11731207
1174- emitStatus ( true ) ;
1175- await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
1208+ expect ( fetchState . orgCalls ) . toBe ( expectedOrgCalls ) ;
1209+ expect ( service . getState ( ) ) . toMatchObject ( expectedState ) ;
11761210
1177- expect ( fetchState . orgCalls ) . toBe ( 0 ) ;
1178- expect ( service . getState ( ) . currentProjectId ) . toBeNull ( ) ;
1179- } ) ;
1211+ // Emitting connectivity should not trigger extra work: the map is
1212+ // complete (or empty by design) and the recovery loop must not spin.
1213+ emitStatus ( true ) ;
1214+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
1215+ expect ( fetchState . orgCalls ) . toBe ( expectedOrgCalls ) ;
1216+ expect ( service . getState ( ) ) . toMatchObject ( expectedState ) ;
1217+ } ,
1218+ ) ;
11801219 } ) ;
11811220
11821221 describe ( "switchOrg" , ( ) => {
0 commit comments