@@ -137,13 +137,15 @@ describe('ThemeService runtime theme tokens', () => {
137137 expect ( rootStyle . getPropertyValue ( '--git-color-branch' ) ) . toBe ( '#a1a1aa' ) ;
138138 expect ( rootStyle . getPropertyValue ( '--git-color-branch-bg' ) ) . toBe ( 'rgba(255, 255, 255, 0.06)' ) ;
139139 expect ( rootStyle . getPropertyValue ( '--git-color-branch-bg-hover' ) ) . toBe ( 'rgba(255, 255, 255, 0.12)' ) ;
140+ expect ( rootStyle . getPropertyValue ( '--git-color-changes' ) ) . toBe ( 'rgb(245, 158, 11)' ) ;
140141 expect ( rootStyle . getPropertyValue ( '--git-color-added' ) ) . toBe ( 'rgb(34, 197, 94)' ) ;
141- expect ( rootStyle . getPropertyValue ( '--git-color-added-bg' ) ) . toBe ( 'rgba(34, 197, 94, 0.1)' ) ;
142- expect ( rootStyle . getPropertyValue ( '--git-color-added-bg-hover' ) ) . toBe ( 'rgba(34, 197, 94, 0.15)' ) ;
143- expect ( rootStyle . getPropertyValue ( '--git-color-changes-bg' ) ) . toBe ( 'rgba(245, 158, 11, 0.1)' ) ;
144- expect ( rootStyle . getPropertyValue ( '--git-color-deleted-bg-hover' ) ) . toBe ( 'rgba(239, 68, 68, 0.15)' ) ;
145- expect ( rootStyle . getPropertyValue ( '--git-color-staged-bg-hover' ) ) . toBe ( 'rgba(34, 197, 94, 0.15)' ) ;
146- expect ( rootStyle . getPropertyValue ( '--git-color-staged-border' ) ) . toBe ( 'rgba(34, 197, 94, 0.3)' ) ;
142+ expect ( rootStyle . getPropertyValue ( '--git-color-deleted' ) ) . toBe ( 'rgb(239, 68, 68)' ) ;
143+ expect ( rootStyle . getPropertyValue ( '--git-color-staged' ) ) . toBe ( 'rgb(34, 197, 94)' ) ;
144+ expect ( rootStyle . getPropertyValue ( '--git-color-changes-bg' ) ) . toBe ( '' ) ;
145+ expect ( rootStyle . getPropertyValue ( '--git-color-added-bg' ) ) . toBe ( '' ) ;
146+ expect ( rootStyle . getPropertyValue ( '--git-color-deleted-bg' ) ) . toBe ( '' ) ;
147+ expect ( rootStyle . getPropertyValue ( '--git-color-staged-bg' ) ) . toBe ( '' ) ;
148+ expect ( rootStyle . getPropertyValue ( '--git-color-staged-border' ) ) . toBe ( '' ) ;
147149 expect ( rootStyle . getPropertyValue ( '--git-color-pull' ) ) . toBe ( '' ) ;
148150 expect ( rootStyle . getPropertyValue ( '--git-color-push' ) ) . toBe ( '' ) ;
149151 } ) ;
@@ -388,7 +390,7 @@ describe('ThemeService runtime theme tokens', () => {
388390 expect ( configAPI . setConfig ) . not . toHaveBeenCalledWith ( 'themes.custom' , expect . anything ( ) ) ;
389391 } ) ;
390392
391- it ( 'does not export non-contract dynamic keys from custom themes' , ( ) => {
393+ it ( 'does not inject non-contract dynamic keys from custom themes' , ( ) => {
392394 const service = new ThemeService ( ) ;
393395 const customTheme = {
394396 ...bitfunLightTheme ,
@@ -616,6 +618,115 @@ describe('ThemeService runtime theme tokens', () => {
616618 ) . rejects . toThrow ( / r e s e r v e d f o r a b u i l t - i n t h e m e / ) ;
617619 } ) ;
618620
621+ it ( 'strips non-contract git color keys from registered custom themes' , async ( ) => {
622+ const nonContractGitColorKeys = [
623+ 'changesBg' ,
624+ 'addedBg' ,
625+ 'deletedBg' ,
626+ 'stagedBg' ,
627+ 'addedBgHover' ,
628+ 'stagedBorder' ,
629+ 'pull' ,
630+ ] as const ;
631+ const expectNoNonContractGitColorKeys = ( gitColors : ThemeConfig [ 'colors' ] [ 'git' ] ) => {
632+ const gitRecord = gitColors as unknown as Record < string , unknown > ;
633+ nonContractGitColorKeys . forEach ( key => {
634+ expect ( gitRecord ) . not . toHaveProperty ( key ) ;
635+ } ) ;
636+ } ;
637+ const service = new ThemeService ( ) ;
638+ const legacyTheme = {
639+ ...bitfunDarkTheme ,
640+ id : 'custom-legacy-git-bg' ,
641+ name : 'Legacy Git Backgrounds' ,
642+ colors : {
643+ ...bitfunDarkTheme . colors ,
644+ git : {
645+ ...bitfunDarkTheme . colors . git ,
646+ changesBg : 'rgba(245, 158, 11, 0.1)' ,
647+ addedBg : 'rgba(34, 197, 94, 0.1)' ,
648+ deletedBg : 'rgba(239, 68, 68, 0.1)' ,
649+ stagedBg : 'rgba(16, 185, 129, 0.1)' ,
650+ addedBgHover : 'rgba(34, 197, 94, 0.2)' ,
651+ stagedBorder : 'rgba(16, 185, 129, 0.4)' ,
652+ pull : '#60a5fa' ,
653+ } ,
654+ } ,
655+ } as unknown as ThemeConfig ;
656+
657+ await service . registerTheme ( legacyTheme ) ;
658+
659+ const normalized = service . getTheme ( 'custom-legacy-git-bg' ) ;
660+ expect ( normalized ) . toBeDefined ( ) ;
661+ if ( ! normalized ) {
662+ throw new Error ( 'Expected custom legacy git theme to be registered' ) ;
663+ }
664+ expect ( normalized . colors . git . added ) . toBe ( bitfunDarkTheme . colors . git . added ) ;
665+ expectNoNonContractGitColorKeys ( normalized . colors . git ) ;
666+
667+ const persistedThemes = vi . mocked ( configAPI . setConfig ) . mock . calls . find ( ( [ key ] ) => key === 'themes.custom' ) ?. [ 1 ] as
668+ | ThemeConfig [ ]
669+ | undefined ;
670+ const persistedTheme = persistedThemes ?. find ( theme => theme . id === 'custom-legacy-git-bg' ) ;
671+ expect ( persistedTheme ) . toBeDefined ( ) ;
672+ if ( ! persistedTheme ) {
673+ throw new Error ( 'Expected custom legacy git theme to be persisted' ) ;
674+ }
675+ expectNoNonContractGitColorKeys ( persistedTheme . colors . git ) ;
676+
677+ const exported = service . exportTheme ( 'custom-legacy-git-bg' ) ;
678+ expect ( exported ) . not . toBeNull ( ) ;
679+ if ( ! exported ) {
680+ throw new Error ( 'Expected custom legacy git theme to be exported' ) ;
681+ }
682+ expectNoNonContractGitColorKeys ( exported . theme . colors . git ) ;
683+ } ) ;
684+
685+ it ( 'migrates persisted custom themes with non-contract git color keys on load' , async ( ) => {
686+ const legacyTheme = {
687+ ...bitfunDarkTheme ,
688+ id : 'custom-loaded-legacy-git' ,
689+ name : 'Loaded Legacy Git' ,
690+ colors : {
691+ ...bitfunDarkTheme . colors ,
692+ git : {
693+ ...bitfunDarkTheme . colors . git ,
694+ changesBg : 'rgba(245, 158, 11, 0.1)' ,
695+ addedBgHover : 'rgba(34, 197, 94, 0.2)' ,
696+ stagedBorder : 'rgba(16, 185, 129, 0.4)' ,
697+ } ,
698+ } ,
699+ } as unknown as ThemeConfig ;
700+ vi . mocked ( configAPI . getConfig ) . mockResolvedValue ( { custom : [ legacyTheme ] } ) ;
701+ const service = new ThemeService ( ) ;
702+
703+ await service . ensureUserThemesLoaded ( ) ;
704+
705+ const normalized = service . getTheme ( 'custom-loaded-legacy-git' ) ;
706+ expect ( normalized ) . toBeDefined ( ) ;
707+ if ( ! normalized ) {
708+ throw new Error ( 'Expected legacy custom theme to load' ) ;
709+ }
710+ expect ( normalized . colors . git . added ) . toBe ( bitfunDarkTheme . colors . git . added ) ;
711+ expect ( normalized . colors . git . staged ) . toBe ( bitfunDarkTheme . colors . git . staged ) ;
712+ expect ( normalized . colors . git as unknown as Record < string , unknown > ) . not . toHaveProperty ( 'changesBg' ) ;
713+ expect ( normalized . colors . git as unknown as Record < string , unknown > ) . not . toHaveProperty ( 'addedBgHover' ) ;
714+ expect ( normalized . colors . git as unknown as Record < string , unknown > ) . not . toHaveProperty ( 'stagedBorder' ) ;
715+
716+ const migratedThemes = vi . mocked ( configAPI . setConfig ) . mock . calls . find ( ( [ key ] ) => key === 'themes.custom' ) ?. [ 1 ] as
717+ | ThemeConfig [ ]
718+ | undefined ;
719+ expect ( migratedThemes ) . toHaveLength ( 1 ) ;
720+ const migratedGitColors = migratedThemes ?. [ 0 ] ?. colors . git as unknown as Record < string , unknown > | undefined ;
721+ expect ( migratedGitColors ) . toBeDefined ( ) ;
722+ if ( ! migratedGitColors ) {
723+ throw new Error ( 'Expected migrated theme to keep git colors' ) ;
724+ }
725+ expect ( migratedGitColors ) . not . toHaveProperty ( 'changesBg' ) ;
726+ expect ( migratedGitColors ) . not . toHaveProperty ( 'addedBgHover' ) ;
727+ expect ( migratedGitColors ) . not . toHaveProperty ( 'stagedBorder' ) ;
728+ } ) ;
729+
619730 it ( 'projects normalized custom themes through the compact plugin color boundary' , async ( ) => {
620731 const service = new ThemeService ( ) ;
621732 const partialCustomTheme = {
0 commit comments