fix(theme): validate accent review boundaries
- commit
- 3389f596fce81d339324734067dcc49950c442db
- author
- brice · · 2026-07-18 12:55 UTC
- parent
- aae2680
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +28 -2
M internal/adapters/config/theme_accent_test.go +6 -1
| @@ -146,5 +146,10 @@ func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) { | |||
| 146 | 146 | } | |
| 147 | 147 | ||
| 148 | 148 | cancel() | |
| 149 | - | require.True(t, errors.Is(<-done, context.Canceled)) | |
| 149 | + | select { | |
| 150 | + | case err := <-done: | |
| 151 | + | require.True(t, errors.Is(err, context.Canceled)) | |
| 152 | + | case <-time.After(2 * time.Second): | |
| 153 | + | t.Fatal("timed out waiting for config watcher shutdown") | |
| 154 | + | } | |
| 150 | 155 | } | |
| @@ -146,5 +146,10 @@ func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) { | |||||
| 146 | } | 146 | } | ||
| 147 | 147 | ||||
| 148 | cancel() | 148 | cancel() | ||
| 149 | - | require.True(t, errors.Is(<-done, context.Canceled)) | 149 | + | select { |
| 150 | + | case err := <-done: | |||
| 151 | + | require.True(t, errors.Is(err, context.Canceled)) | |||
| 152 | + | case <-time.After(2 * time.Second): | |||
| 153 | + | t.Fatal("timed out waiting for config watcher shutdown") | |||
| 154 | + | } | |||
| 150 | } | 155 | } | ||
M internal/usecase/theme/accent_resolve.go +4 -1
| @@ -55,7 +55,10 @@ func ResolveAccent(t Theme, policy domain.ThemeAccent) Accent { | |||
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | func resolveExplicitAccent(t Theme, slot uint8) Accent { | |
| 58 | - | if slot >= uint8(len(t.Palette)) || t.PaletteKnown&(uint16(1)<<slot) == 0 { | |
| 58 | + | if slot >= uint8(len(t.Palette)) { | |
| 59 | + | return Accent{} | |
| 60 | + | } | |
| 61 | + | if t.PaletteKnown&(uint16(1)<<slot) == 0 { | |
| 59 | 62 | // An explicit policy is a strict override. Even without OSC RGB for its | |
| 60 | 63 | // slot, it may still safely decorate foregrounds and borders by index. | |
| 61 | 64 | return indexedAccent(slot) | |
| @@ -55,7 +55,10 @@ func ResolveAccent(t Theme, policy domain.ThemeAccent) Accent { | |||||
| 55 | } | 55 | } | ||
| 56 | 56 | ||||
| 57 | func resolveExplicitAccent(t Theme, slot uint8) Accent { | 57 | func resolveExplicitAccent(t Theme, slot uint8) Accent { | ||
| 58 | - | if slot >= uint8(len(t.Palette)) || t.PaletteKnown&(uint16(1)<<slot) == 0 { | 58 | + | if slot >= uint8(len(t.Palette)) { |
| 59 | + | return Accent{} | |||
| 60 | + | } | |||
| 61 | + | if t.PaletteKnown&(uint16(1)<<slot) == 0 { | |||
| 59 | // An explicit policy is a strict override. Even without OSC RGB for its | 62 | // An explicit policy is a strict override. Even without OSC RGB for its | ||
| 60 | // slot, it may still safely decorate foregrounds and borders by index. | 63 | // slot, it may still safely decorate foregrounds and borders by index. | ||
| 61 | return indexedAccent(slot) | 64 | return indexedAccent(slot) | ||
M internal/usecase/theme/accent_resolve_test.go +18 -0
| @@ -158,6 +158,24 @@ func TestResolveAccentExplicitSlot(t *testing.T) { | |||
| 158 | 158 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 5}, | |
| 159 | 159 | want: Accent{Slot: 5, IndexedOnly: true}, | |
| 160 | 160 | }, | |
| 161 | + | { | |
| 162 | + | name: "unknown final palette slot keeps its indexed decoration", | |
| 163 | + | theme: paletteTheme(map[int]renderer.RGB{2: teal, 10: teal, 4: blue}), | |
| 164 | + | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 15}, | |
| 165 | + | want: Accent{Slot: 15, IndexedOnly: true}, | |
| 166 | + | }, | |
| 167 | + | { | |
| 168 | + | name: "first slot outside palette is rejected", | |
| 169 | + | theme: paletteTheme(map[int]renderer.RGB{2: teal, 10: teal, 4: blue}), | |
| 170 | + | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 16}, | |
| 171 | + | want: Accent{}, | |
| 172 | + | }, | |
| 173 | + | { | |
| 174 | + | name: "maximum uint8 slot outside palette is rejected", | |
| 175 | + | theme: paletteTheme(map[int]renderer.RGB{2: teal, 10: teal, 4: blue}), | |
| 176 | + | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 255}, | |
| 177 | + | want: Accent{}, | |
| 178 | + | }, | |
| 161 | 179 | { | |
| 162 | 180 | name: "explicit known slot is indexed only without truecolor prerequisites", | |
| 163 | 181 | theme: func() Theme { t := paletteTheme(map[int]renderer.RGB{3: teal}); t.TrueColor = false; return t }(), | |
| @@ -158,6 +158,24 @@ func TestResolveAccentExplicitSlot(t *testing.T) { | |||||
| 158 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 5}, | 158 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 5}, | ||
| 159 | want: Accent{Slot: 5, IndexedOnly: true}, | 159 | want: Accent{Slot: 5, IndexedOnly: true}, | ||
| 160 | }, | 160 | }, | ||
| 161 | + | { | |||
| 162 | + | name: "unknown final palette slot keeps its indexed decoration", | |||
| 163 | + | theme: paletteTheme(map[int]renderer.RGB{2: teal, 10: teal, 4: blue}), | |||
| 164 | + | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 15}, | |||
| 165 | + | want: Accent{Slot: 15, IndexedOnly: true}, | |||
| 166 | + | }, | |||
| 167 | + | { | |||
| 168 | + | name: "first slot outside palette is rejected", | |||
| 169 | + | theme: paletteTheme(map[int]renderer.RGB{2: teal, 10: teal, 4: blue}), | |||
| 170 | + | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 16}, | |||
| 171 | + | want: Accent{}, | |||
| 172 | + | }, | |||
| 173 | + | { | |||
| 174 | + | name: "maximum uint8 slot outside palette is rejected", | |||
| 175 | + | theme: paletteTheme(map[int]renderer.RGB{2: teal, 10: teal, 4: blue}), | |||
| 176 | + | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 255}, | |||
| 177 | + | want: Accent{}, | |||
| 178 | + | }, | |||
| 161 | { | 179 | { | ||
| 162 | name: "explicit known slot is indexed only without truecolor prerequisites", | 180 | name: "explicit known slot is indexed only without truecolor prerequisites", | ||
| 163 | theme: func() Theme { t := paletteTheme(map[int]renderer.RGB{3: teal}); t.TrueColor = false; return t }(), | 181 | theme: func() Theme { t := paletteTheme(map[int]renderer.RGB{3: teal}); t.TrueColor = false; return t }(), | ||