@@ -2685,11 +2685,274 @@ describe('Settings Loading and Merging', () => {
26852685 expect ( process . env [ 'TESTTEST' ] ) . toEqual ( '1234' ) ;
26862686 } ) ;
26872687
2688- it ( 'does not load env files from untrusted spaces' , ( ) => {
2689- setup ( { isFolderTrustEnabled : true , isWorkspaceTrustedValue : false } ) ;
2688+ it ( 'does not load project .env files from untrusted workspaces' , ( ) => {
2689+ delete process . env [ 'PROJECT_ENV_VAR' ] ;
2690+ const cwdSpy = vi
2691+ . spyOn ( process , 'cwd' )
2692+ . mockReturnValue ( MOCK_WORKSPACE_DIR ) ;
2693+
2694+ const projectEnvPath = path . join ( MOCK_WORKSPACE_DIR , '.env' ) ;
2695+
2696+ vi . mocked ( isWorkspaceTrusted ) . mockReturnValue ( {
2697+ isTrusted : false ,
2698+ source : 'file' ,
2699+ } ) ;
2700+ ( mockFsExistsSync as Mock ) . mockImplementation ( ( p : fs . PathLike ) =>
2701+ [ USER_SETTINGS_PATH , projectEnvPath ] . includes ( p . toString ( ) ) ,
2702+ ) ;
2703+ const userSettingsContent : Settings = {
2704+ ui : {
2705+ theme : 'dark' ,
2706+ } ,
2707+ security : {
2708+ folderTrust : {
2709+ enabled : true ,
2710+ } ,
2711+ } ,
2712+ } ;
2713+ ( fs . readFileSync as Mock ) . mockImplementation (
2714+ ( p : fs . PathOrFileDescriptor ) => {
2715+ if ( p === USER_SETTINGS_PATH )
2716+ return JSON . stringify ( userSettingsContent ) ;
2717+ if ( p === projectEnvPath ) return 'PROJECT_ENV_VAR=from_project' ;
2718+ return '{}' ;
2719+ } ,
2720+ ) ;
2721+
26902722 loadEnvironment ( loadSettings ( MOCK_WORKSPACE_DIR ) . merged ) ;
26912723
2692- expect ( process . env [ 'TESTTEST' ] ) . not . toEqual ( '1234' ) ;
2724+ // Project .env should NOT be loaded when workspace is untrusted
2725+ expect ( process . env [ 'PROJECT_ENV_VAR' ] ) . toBeUndefined ( ) ;
2726+ cwdSpy . mockRestore ( ) ;
2727+ } ) ;
2728+
2729+ describe ( 'settings.env field' , ( ) => {
2730+ const originalEnv = { ...process . env } ;
2731+
2732+ beforeEach ( ( ) => {
2733+ process . env = { ...originalEnv } ;
2734+ delete process . env [ 'ENV_FROM_SETTINGS' ] ;
2735+ delete process . env [ 'ENV_OVERRIDE_TEST' ] ;
2736+ delete process . env [ 'SYSTEM_ENV_VAR' ] ;
2737+ delete process . env [ 'MULTI_VAR_A' ] ;
2738+ delete process . env [ 'MULTI_VAR_B' ] ;
2739+ delete process . env [ 'MULTI_VAR_C' ] ;
2740+ delete process . env [ 'USER_ENV_VAR' ] ;
2741+ delete process . env [ 'WORKSPACE_ENV_VAR' ] ;
2742+ } ) ;
2743+
2744+ afterEach ( ( ) => {
2745+ process . env = originalEnv ;
2746+ } ) ;
2747+
2748+ it ( 'should load environment variables from settings.env as fallback' , ( ) => {
2749+ const userSettingsContent : Settings = {
2750+ env : {
2751+ ENV_FROM_SETTINGS : 'settings_value' ,
2752+ } ,
2753+ } ;
2754+
2755+ ( mockFsExistsSync as Mock ) . mockImplementation ( ( p : fs . PathLike ) =>
2756+ [ USER_SETTINGS_PATH ] . includes ( p . toString ( ) ) ,
2757+ ) ;
2758+ ( fs . readFileSync as Mock ) . mockImplementation (
2759+ ( p : fs . PathOrFileDescriptor ) => {
2760+ if ( p === USER_SETTINGS_PATH )
2761+ return JSON . stringify ( userSettingsContent ) ;
2762+ return '{}' ;
2763+ } ,
2764+ ) ;
2765+
2766+ vi . mocked ( isWorkspaceTrusted ) . mockReturnValue ( {
2767+ isTrusted : true ,
2768+ source : 'file' ,
2769+ } ) ;
2770+
2771+ // loadSettings internally calls loadEnvironment with userSettings
2772+ loadSettings ( MOCK_WORKSPACE_DIR ) ;
2773+
2774+ expect ( process . env [ 'ENV_FROM_SETTINGS' ] ) . toEqual ( 'settings_value' ) ;
2775+ } ) ;
2776+
2777+ it ( 'should allow .env file to override settings.env values' , ( ) => {
2778+ const geminiEnvPath = path . resolve ( path . join ( QWEN_DIR , '.env' ) ) ;
2779+ const userSettingsContent : Settings = {
2780+ env : {
2781+ ENV_OVERRIDE_TEST : 'from_settings' ,
2782+ } ,
2783+ } ;
2784+
2785+ ( mockFsExistsSync as Mock ) . mockImplementation ( ( p : fs . PathLike ) =>
2786+ [ USER_SETTINGS_PATH , geminiEnvPath ] . includes ( p . toString ( ) ) ,
2787+ ) ;
2788+ ( fs . readFileSync as Mock ) . mockImplementation (
2789+ ( p : fs . PathOrFileDescriptor ) => {
2790+ if ( p === USER_SETTINGS_PATH )
2791+ return JSON . stringify ( userSettingsContent ) ;
2792+ if ( p === geminiEnvPath ) return 'ENV_OVERRIDE_TEST=from_dotenv' ;
2793+ return '{}' ;
2794+ } ,
2795+ ) ;
2796+
2797+ vi . mocked ( isWorkspaceTrusted ) . mockReturnValue ( {
2798+ isTrusted : true ,
2799+ source : 'file' ,
2800+ } ) ;
2801+
2802+ // loadSettings internally calls loadEnvironment with merged settings
2803+ loadSettings ( MOCK_WORKSPACE_DIR ) ;
2804+
2805+ // .env file has higher priority than settings.env (loaded first, no-override)
2806+ expect ( process . env [ 'ENV_OVERRIDE_TEST' ] ) . toEqual ( 'from_dotenv' ) ;
2807+ } ) ;
2808+
2809+ it ( 'should not override existing system environment variables' , ( ) => {
2810+ process . env [ 'SYSTEM_ENV_VAR' ] = 'system_value' ;
2811+
2812+ const geminiEnvPath = path . resolve ( path . join ( QWEN_DIR , '.env' ) ) ;
2813+ const userSettingsContent : Settings = {
2814+ env : {
2815+ SYSTEM_ENV_VAR : 'from_settings' ,
2816+ } ,
2817+ } ;
2818+
2819+ ( mockFsExistsSync as Mock ) . mockImplementation ( ( p : fs . PathLike ) =>
2820+ [ USER_SETTINGS_PATH , geminiEnvPath ] . includes ( p . toString ( ) ) ,
2821+ ) ;
2822+ ( fs . readFileSync as Mock ) . mockImplementation (
2823+ ( p : fs . PathOrFileDescriptor ) => {
2824+ if ( p === USER_SETTINGS_PATH )
2825+ return JSON . stringify ( userSettingsContent ) ;
2826+ if ( p === geminiEnvPath ) return 'SYSTEM_ENV_VAR=from_dotenv' ;
2827+ return '{}' ;
2828+ } ,
2829+ ) ;
2830+
2831+ vi . mocked ( isWorkspaceTrusted ) . mockReturnValue ( {
2832+ isTrusted : true ,
2833+ source : 'file' ,
2834+ } ) ;
2835+
2836+ // loadSettings internally calls loadEnvironment with userSettings
2837+ loadSettings ( MOCK_WORKSPACE_DIR ) ;
2838+
2839+ // System environment variable should have highest priority
2840+ expect ( process . env [ 'SYSTEM_ENV_VAR' ] ) . toEqual ( 'system_value' ) ;
2841+ } ) ;
2842+
2843+ it ( 'should support multiple env variables in settings.env' , ( ) => {
2844+ const userSettingsContent : Settings = {
2845+ env : {
2846+ MULTI_VAR_A : 'value_a' ,
2847+ MULTI_VAR_B : 'value_b' ,
2848+ MULTI_VAR_C : 'value_c' ,
2849+ } ,
2850+ } ;
2851+
2852+ ( mockFsExistsSync as Mock ) . mockImplementation ( ( p : fs . PathLike ) =>
2853+ [ USER_SETTINGS_PATH ] . includes ( p . toString ( ) ) ,
2854+ ) ;
2855+ ( fs . readFileSync as Mock ) . mockImplementation (
2856+ ( p : fs . PathOrFileDescriptor ) => {
2857+ if ( p === USER_SETTINGS_PATH )
2858+ return JSON . stringify ( userSettingsContent ) ;
2859+ return '{}' ;
2860+ } ,
2861+ ) ;
2862+
2863+ vi . mocked ( isWorkspaceTrusted ) . mockReturnValue ( {
2864+ isTrusted : true ,
2865+ source : 'file' ,
2866+ } ) ;
2867+
2868+ // loadSettings internally calls loadEnvironment with userSettings
2869+ loadSettings ( MOCK_WORKSPACE_DIR ) ;
2870+
2871+ expect ( process . env [ 'MULTI_VAR_A' ] ) . toEqual ( 'value_a' ) ;
2872+ expect ( process . env [ 'MULTI_VAR_B' ] ) . toEqual ( 'value_b' ) ;
2873+ expect ( process . env [ 'MULTI_VAR_C' ] ) . toEqual ( 'value_c' ) ;
2874+ } ) ;
2875+
2876+ it ( 'should load settings.env from both user and workspace settings' , ( ) => {
2877+ const workspaceSettingsContent = {
2878+ env : {
2879+ WORKSPACE_ENV_VAR : 'workspace_value' ,
2880+ } ,
2881+ } ;
2882+ const userSettingsContent : Settings = {
2883+ env : {
2884+ USER_ENV_VAR : 'user_value' ,
2885+ } ,
2886+ } ;
2887+
2888+ ( mockFsExistsSync as Mock ) . mockImplementation ( ( p : fs . PathLike ) =>
2889+ [ USER_SETTINGS_PATH , MOCK_WORKSPACE_SETTINGS_PATH ] . includes (
2890+ p . toString ( ) ,
2891+ ) ,
2892+ ) ;
2893+ ( fs . readFileSync as Mock ) . mockImplementation (
2894+ ( p : fs . PathOrFileDescriptor ) => {
2895+ if ( p === USER_SETTINGS_PATH )
2896+ return JSON . stringify ( userSettingsContent ) ;
2897+ if ( p === MOCK_WORKSPACE_SETTINGS_PATH )
2898+ return JSON . stringify ( workspaceSettingsContent ) ;
2899+ return '{}' ;
2900+ } ,
2901+ ) ;
2902+
2903+ vi . mocked ( isWorkspaceTrusted ) . mockReturnValue ( {
2904+ isTrusted : true ,
2905+ source : 'file' ,
2906+ } ) ;
2907+
2908+ // loadSettings internally calls loadEnvironment with merged settings
2909+ loadSettings ( MOCK_WORKSPACE_DIR ) ;
2910+
2911+ // Both user-level and workspace-level env should be loaded
2912+ expect ( process . env [ 'USER_ENV_VAR' ] ) . toEqual ( 'user_value' ) ;
2913+ expect ( process . env [ 'WORKSPACE_ENV_VAR' ] ) . toEqual ( 'workspace_value' ) ;
2914+ } ) ;
2915+
2916+ it ( 'should load user-level settings.env even when workspace is untrusted' , ( ) => {
2917+ const userSettingsContent : Settings = {
2918+ env : {
2919+ USER_ENV_VAR : 'user_value' ,
2920+ } ,
2921+ } ;
2922+ const workspaceSettingsContent = {
2923+ env : {
2924+ WORKSPACE_ENV_VAR : 'workspace_value' ,
2925+ } ,
2926+ } ;
2927+
2928+ ( mockFsExistsSync as Mock ) . mockImplementation ( ( p : fs . PathLike ) =>
2929+ [ USER_SETTINGS_PATH , MOCK_WORKSPACE_SETTINGS_PATH ] . includes (
2930+ p . toString ( ) ,
2931+ ) ,
2932+ ) ;
2933+ ( fs . readFileSync as Mock ) . mockImplementation (
2934+ ( p : fs . PathOrFileDescriptor ) => {
2935+ if ( p === USER_SETTINGS_PATH )
2936+ return JSON . stringify ( userSettingsContent ) ;
2937+ if ( p === MOCK_WORKSPACE_SETTINGS_PATH )
2938+ return JSON . stringify ( workspaceSettingsContent ) ;
2939+ return '{}' ;
2940+ } ,
2941+ ) ;
2942+
2943+ // Workspace is untrusted
2944+ vi . mocked ( isWorkspaceTrusted ) . mockReturnValue ( {
2945+ isTrusted : false ,
2946+ source : 'file' ,
2947+ } ) ;
2948+
2949+ loadSettings ( MOCK_WORKSPACE_DIR ) ;
2950+
2951+ // User-level settings.env should still be loaded even when untrusted
2952+ expect ( process . env [ 'USER_ENV_VAR' ] ) . toEqual ( 'user_value' ) ;
2953+ // Workspace-level settings.env should NOT be loaded (filtered by mergeSettings)
2954+ expect ( process . env [ 'WORKSPACE_ENV_VAR' ] ) . toBeUndefined ( ) ;
2955+ } ) ;
26932956 } ) ;
26942957 } ) ;
26952958
0 commit comments