@@ -28,8 +28,9 @@ import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticati
2828import { IWorkbenchEnvironmentService } from '../../../environment/common/environmentService.js' ;
2929import { IExtensionService } from '../../../extensions/common/extensions.js' ;
3030import { IHostService } from '../../../host/browser/host.js' ;
31- import { DefaultAccountProvider } from '../../browser/defaultAccount.js' ;
31+ import { DefaultAccountProvider , DefaultAccountService } from '../../browser/defaultAccount.js' ;
3232import { TestProductService } from '../../../../test/common/workbenchTestServices.js' ;
33+ import { AccountPolicyGateState , AccountPolicyService } from '../../../policies/common/accountPolicyService.js' ;
3334
3435suite ( 'DefaultAccountProvider' , ( ) => {
3536
@@ -575,6 +576,167 @@ suite('DefaultAccountProvider', () => {
575576 assert . strictEqual ( requestService . requestCount , 2 ) ;
576577 } ) ;
577578
579+ test ( 'managed settings endpoint outage without a refresh requirement keeps freshness non-blocking' , async ( ) => {
580+ let managedSettingsRequestCount = 0 ;
581+ const requestService = new TestRequestService ( async options => {
582+ if ( options . url ?. endsWith ( '/copilot_internal/user' ) ) {
583+ return jsonResponse ( { chat_enabled : true } ) ;
584+ }
585+ if ( options . url ?. includes ( '/copilot_internal/managed_settings' ) ) {
586+ managedSettingsRequestCount ++ ;
587+ if ( managedSettingsRequestCount === 1 ) {
588+ return jsonResponse ( {
589+ permissions : { disableBypassPermissionsMode : 'disable' } ,
590+ } ) ;
591+ }
592+ throw new Error ( 'managed settings request timed out' ) ;
593+ }
594+ throw new Error ( `Unexpected request: ${ options . url } ` ) ;
595+ } ) ;
596+ const provider = await createProvider (
597+ requestService ,
598+ { } ,
599+ { } ,
600+ 'https://api.github.com/copilot_internal/managed_settings' ,
601+ { getSessions : async ( ) => sessions }
602+ ) ;
603+
604+ await provider . refresh ( { forceRefresh : true } ) ;
605+ const accountService = disposables . add ( new DefaultAccountService ( TestProductService ) ) ;
606+ accountService . setDefaultAccountProvider ( provider ) ;
607+ await accountService . refresh ( ) ;
608+ const gateService = disposables . add ( new AccountPolicyService ( new NullLogService ( ) , accountService ) ) ;
609+
610+ assert . deepStrictEqual ( {
611+ managedSettingsRequestCount,
612+ requestTimeouts : requestService . requests
613+ . filter ( request => request . url ?. includes ( '/copilot_internal/managed_settings' ) )
614+ . map ( request => request . timeout ) ,
615+ defaultAccount : provider . defaultAccount ,
616+ managedSettings : provider . policyData ?. managedSettings ,
617+ freshness : provider . managedSettingsFreshness ,
618+ compatibilityError : provider . managedSettingsCompatibilityError ,
619+ gateState : gateService . gateInfo . state ,
620+ } , {
621+ managedSettingsRequestCount : 2 ,
622+ requestTimeouts : [ 5000 , 5000 ] ,
623+ defaultAccount : {
624+ authenticationProvider : { id : 'github' , name : 'GitHub' , enterprise : false } ,
625+ accountName : 'octocat' ,
626+ sessionId : 'session' ,
627+ enterprise : false ,
628+ entitlementsData : { chat_enabled : true } ,
629+ } ,
630+ managedSettings : {
631+ 'permissions.disableBypassPermissionsMode' : 'disable' ,
632+ } ,
633+ freshness : { state : ManagedSettingsFreshnessState . NotRequired } ,
634+ compatibilityError : null ,
635+ gateState : AccountPolicyGateState . Inactive ,
636+ } ) ;
637+ } ) ;
638+
639+ test ( 'truthy non-boolean server refresh control remains non-blocking after an endpoint outage' , async ( ) => {
640+ let managedSettingsRequestCount = 0 ;
641+ const requestService = new TestRequestService ( async options => {
642+ if ( options . url ?. endsWith ( '/copilot_internal/user' ) ) {
643+ return jsonResponse ( { chat_enabled : true } ) ;
644+ }
645+ if ( options . url ?. includes ( '/copilot_internal/managed_settings' ) ) {
646+ managedSettingsRequestCount ++ ;
647+ if ( managedSettingsRequestCount === 1 ) {
648+ return jsonResponse ( {
649+ forceRemoteSettingsRefresh : 'true' ,
650+ permissions : { disableBypassPermissionsMode : 'disable' } ,
651+ } ) ;
652+ }
653+ throw new Error ( 'managed settings request timed out' ) ;
654+ }
655+ throw new Error ( `Unexpected request: ${ options . url } ` ) ;
656+ } ) ;
657+ const provider = await createProvider (
658+ requestService ,
659+ { } ,
660+ { } ,
661+ 'https://api.github.com/copilot_internal/managed_settings' ,
662+ { getSessions : async ( ) => sessions }
663+ ) ;
664+
665+ await provider . refresh ( { forceRefresh : true } ) ;
666+
667+ assert . deepStrictEqual ( {
668+ managedSettingsRequestCount,
669+ managedSettings : provider . policyData ?. managedSettings ,
670+ freshness : provider . managedSettingsFreshness ,
671+ compatibilityError : provider . managedSettingsCompatibilityError ,
672+ } , {
673+ managedSettingsRequestCount : 2 ,
674+ managedSettings : {
675+ forceRemoteSettingsRefresh : 'true' ,
676+ 'permissions.disableBypassPermissionsMode' : 'disable' ,
677+ } ,
678+ freshness : { state : ManagedSettingsFreshnessState . NotRequired } ,
679+ compatibilityError : null ,
680+ } ) ;
681+ } ) ;
682+
683+ test ( 'non-466 managed settings failures remain non-blocking without a refresh requirement' , async ( ) => {
684+ const failures : { name : string ; response : ( ) => IRequestContext } [ ] = [
685+ { name : 'http error' , response : ( ) => jsonResponse ( { } , 500 ) } ,
686+ {
687+ name : 'malformed response' ,
688+ response : ( ) => ( {
689+ res : { statusCode : 200 , headers : { } } ,
690+ stream : bufferToStream ( VSBuffer . fromString ( '{' ) ) ,
691+ } ) ,
692+ } ,
693+ { name : 'rate limited' , response : ( ) => jsonResponse ( { } , 429 , { 'retry-after' : '60' } ) } ,
694+ ] ;
695+ const outcomes = [ ] ;
696+
697+ for ( const failure of failures ) {
698+ const requestService = new TestRequestService ( async ( ) => failure . response ( ) ) ;
699+ const provider = await createProvider ( requestService ) ;
700+ const cachedPolicy = createCachedPolicy ( false ) ;
701+ const result = await provider [ 'getManagedSettings' ] ( sessions , cachedPolicy , { forceRefresh : true } ) ;
702+ outcomes . push ( {
703+ name : failure . name ,
704+ status : provider . managedSettingsFetchStatus ,
705+ data : result . data ,
706+ keptCachedTimestamp : result . fetchedAt === cachedPolicy . managedSettingsFetchedAt ,
707+ freshness : provider . managedSettingsFreshness ,
708+ compatibilityError : provider . managedSettingsCompatibilityError ,
709+ } ) ;
710+ }
711+
712+ assert . deepStrictEqual ( outcomes , [
713+ {
714+ name : 'http error' ,
715+ status : 500 ,
716+ data : createCachedPolicy ( false ) . policyData ,
717+ keptCachedTimestamp : true ,
718+ freshness : { state : ManagedSettingsFreshnessState . NotRequired } ,
719+ compatibilityError : null ,
720+ } ,
721+ {
722+ name : 'malformed response' ,
723+ status : 'parse-error' ,
724+ data : createCachedPolicy ( false ) . policyData ,
725+ keptCachedTimestamp : true ,
726+ freshness : { state : ManagedSettingsFreshnessState . NotRequired } ,
727+ compatibilityError : null ,
728+ } ,
729+ {
730+ name : 'rate limited' ,
731+ status : 429 ,
732+ data : createCachedPolicy ( false ) . policyData ,
733+ keptCachedTimestamp : true ,
734+ freshness : { state : ManagedSettingsFreshnessState . NotRequired } ,
735+ compatibilityError : null ,
736+ } ,
737+ ] ) ;
738+ } ) ;
739+
578740 test ( 'managed settings source change preserves a prior blocked state' , async ( ) => {
579741 const requestService = new TestRequestService ( async ( ) => {
580742 throw new Error ( 'managed settings unavailable' ) ;
0 commit comments