@@ -479,6 +479,137 @@ describe('Settings Loading and Merging', () => {
479479 expect ( settings . merged . security ?. folderTrust ?. enabled ) . toBe ( false ) ; // Workspace setting should be used
480480 } ) ;
481481
482+ it ( 'should resolve environment variables and cast them to correct types before validation' , ( ) => {
483+ vi . stubEnv ( 'TEST_AUTO_THEME' , 'false' ) ;
484+ vi . stubEnv ( 'TEST_MAX_TURNS' , '15' ) ;
485+
486+ ( mockFsExistsSync as Mock ) . mockImplementation (
487+ ( p : fs . PathLike ) =>
488+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH ) ,
489+ ) ;
490+ ( fs . readFileSync as Mock ) . mockImplementation (
491+ ( p : fs . PathOrFileDescriptor ) => {
492+ if (
493+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH )
494+ ) {
495+ return JSON . stringify ( {
496+ ui : { autoThemeSwitching : '$TEST_AUTO_THEME' } ,
497+ model : { maxSessionTurns : '$TEST_MAX_TURNS' } ,
498+ } ) ;
499+ }
500+ return '{}' ;
501+ } ,
502+ ) ;
503+
504+ const settings = loadSettings ( MOCK_WORKSPACE_DIR ) ;
505+
506+ expect ( settings . merged . ui . autoThemeSwitching ) . toBe ( false ) ;
507+ expect ( settings . merged . model . maxSessionTurns ) . toBe ( 15 ) ;
508+ expect ( settings . errors ) . toHaveLength ( 0 ) ;
509+ } ) ;
510+
511+ it ( 'should use default values from environment variable placeholders' , ( ) => {
512+ vi . stubEnv ( 'TEST_AUTO_THEME' , '' ) ; // Should trigger default
513+ delete process . env [ 'TEST_AUTO_THEME' ] ;
514+
515+ ( mockFsExistsSync as Mock ) . mockImplementation (
516+ ( p : fs . PathLike ) =>
517+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH ) ,
518+ ) ;
519+ ( fs . readFileSync as Mock ) . mockImplementation (
520+ ( p : fs . PathOrFileDescriptor ) => {
521+ if (
522+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH )
523+ ) {
524+ return JSON . stringify ( {
525+ ui : { autoThemeSwitching : '${TEST_AUTO_THEME:-true}' } ,
526+ } ) ;
527+ }
528+ return '{}' ;
529+ } ,
530+ ) ;
531+
532+ const settings = loadSettings ( MOCK_WORKSPACE_DIR ) ;
533+
534+ expect ( settings . merged . ui . autoThemeSwitching ) . toBe ( true ) ;
535+ expect ( settings . errors ) . toHaveLength ( 0 ) ;
536+ } ) ;
537+
538+ it ( 'should record validation errors if expansion result is invalid' , ( ) => {
539+ vi . stubEnv ( 'TEST_MAX_TURNS' , 'not-a-number' ) ;
540+
541+ ( mockFsExistsSync as Mock ) . mockImplementation (
542+ ( p : fs . PathLike ) =>
543+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH ) ,
544+ ) ;
545+ ( fs . readFileSync as Mock ) . mockImplementation (
546+ ( p : fs . PathOrFileDescriptor ) => {
547+ if (
548+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH )
549+ ) {
550+ return JSON . stringify ( {
551+ model : { maxSessionTurns : '$TEST_MAX_TURNS' } ,
552+ } ) ;
553+ }
554+ return '{}' ;
555+ } ,
556+ ) ;
557+
558+ const settings = loadSettings ( MOCK_WORKSPACE_DIR ) ;
559+
560+ expect ( settings . errors . length ) . toBeGreaterThan ( 0 ) ;
561+ expect ( settings . errors [ 0 ] . message ) . toContain (
562+ 'Expected number, received string' ,
563+ ) ;
564+ // Should fall back to the expanded string value
565+ expect ( settings . merged . model . maxSessionTurns ) . toBe ( 'not-a-number' ) ;
566+ } ) ;
567+
568+ it ( 'should preserve environment variable placeholders on save' , ( ) => {
569+ vi . stubEnv ( 'TEST_AUTO_THEME' , 'true' ) ;
570+ const placeholder = '${TEST_AUTO_THEME:-false}' ;
571+
572+ ( mockFsExistsSync as Mock ) . mockImplementation (
573+ ( p : fs . PathLike ) =>
574+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH ) ,
575+ ) ;
576+ ( fs . readFileSync as Mock ) . mockImplementation (
577+ ( p : fs . PathOrFileDescriptor ) => {
578+ if (
579+ path . normalize ( p . toString ( ) ) === path . normalize ( USER_SETTINGS_PATH )
580+ ) {
581+ return JSON . stringify ( {
582+ ui : { autoThemeSwitching : placeholder } ,
583+ } ) ;
584+ }
585+ return '{}' ;
586+ } ,
587+ ) ;
588+
589+ // Load settings - this will expand the placeholder for runtime use
590+ const loaded = loadSettings ( MOCK_WORKSPACE_DIR ) ;
591+ expect ( loaded . merged . ui . autoThemeSwitching ) . toBe ( true ) ;
592+
593+ // Verify that the original settings for the user scope still have the placeholder
594+ const userFile = loaded . forScope ( SettingScope . User ) ;
595+ expect ( userFile . originalSettings . ui ?. autoThemeSwitching ) . toBe (
596+ placeholder ,
597+ ) ;
598+
599+ // Save settings - this should use the originalSettings (with placeholders)
600+ const mockUpdate = vi . mocked ( updateSettingsFilePreservingFormat ) ;
601+ saveSettings ( userFile ) ;
602+
603+ expect ( mockUpdate ) . toHaveBeenCalledWith (
604+ USER_SETTINGS_PATH ,
605+ expect . objectContaining ( {
606+ ui : expect . objectContaining ( {
607+ autoThemeSwitching : placeholder ,
608+ } ) ,
609+ } ) ,
610+ ) ;
611+ } ) ;
612+
482613 it ( 'should use system folderTrust over user setting' , ( ) => {
483614 ( mockFsExistsSync as Mock ) . mockReturnValue ( true ) ;
484615 const userSettingsContent = {
0 commit comments