feat(config): detect outdated config format and point to migrate command
Add checkLegacyFormat() to the Load() pipeline: when legacy sections ([rendering], [performance], [privacy], [runtime]) are present but [engine] is absent, refuse to load and direct users to run "dumber migrate".
- commit
- 005fae924a748ea22ea3b2b4134d3d0827cf33cb
- author
- brice · · 2026-03-15 16:10 UTC
- parent
- 29ea63e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +103 -0
M internal/infrastructure/config/loader.go +27 -0
| @@ -87,6 +87,10 @@ func (m *Manager) Load() error { | |||
| 87 | 87 | return err | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | + | if err := m.checkLegacyFormat(); err != nil { | |
| 91 | + | return err | |
| 92 | + | } | |
| 93 | + | ||
| 90 | 94 | config, err := m.unmarshalConfig() | |
| 91 | 95 | if err != nil { | |
| 92 | 96 | return err | |
| @@ -151,6 +155,29 @@ func (m *Manager) transformLegacyActionBindings() { | |||
| 151 | 155 | } | |
| 152 | 156 | } | |
| 153 | 157 | ||
| 158 | + | // checkLegacyFormat detects old config format and returns an error directing user to migrate. | |
| 159 | + | func (m *Manager) checkLegacyFormat() error { | |
| 160 | + | // Check if old sections exist by looking for known keys. | |
| 161 | + | // IsSet returns true only for explicitly set values (not SetDefault), so this | |
| 162 | + | // correctly identifies keys present in the config file, env vars, or overrides. | |
| 163 | + | hasOldSections := m.viper.IsSet("rendering.mode") || | |
| 164 | + | m.viper.IsSet("rendering.disable_dmabuf_renderer") || | |
| 165 | + | m.viper.IsSet("performance.profile") || | |
| 166 | + | m.viper.IsSet("privacy.cookie_policy") || | |
| 167 | + | m.viper.IsSet("runtime.prefix") | |
| 168 | + | ||
| 169 | + | hasEngineSection := m.viper.IsSet("engine.type") | |
| 170 | + | ||
| 171 | + | if hasOldSections && !hasEngineSection { | |
| 172 | + | return fmt.Errorf( | |
| 173 | + | "config format outdated: [rendering], [performance], [privacy] sections " + | |
| 174 | + | "have moved to [engine]/[engine.webkit].\n" + | |
| 175 | + | "Run \"dumber migrate\" to update your config file.", | |
| 176 | + | ) | |
| 177 | + | } | |
| 178 | + | return nil | |
| 179 | + | } | |
| 180 | + | ||
| 154 | 181 | func (m *Manager) unmarshalConfig() (*Config, error) { | |
| 155 | 182 | config := &Config{} | |
| 156 | 183 | if err := m.viper.Unmarshal(config); err != nil { | |
| @@ -87,6 +87,10 @@ func (m *Manager) Load() error { | |||||
| 87 | return err | 87 | return err | ||
| 88 | } | 88 | } | ||
| 89 | 89 | ||||
| 90 | + | if err := m.checkLegacyFormat(); err != nil { | |||
| 91 | + | return err | |||
| 92 | + | } | |||
| 93 | + | ||||
| 90 | config, err := m.unmarshalConfig() | 94 | config, err := m.unmarshalConfig() | ||
| 91 | if err != nil { | 95 | if err != nil { | ||
| 92 | return err | 96 | return err | ||
| @@ -151,6 +155,29 @@ func (m *Manager) transformLegacyActionBindings() { | |||||
| 151 | } | 155 | } | ||
| 152 | } | 156 | } | ||
| 153 | 157 | ||||
| 158 | + | // checkLegacyFormat detects old config format and returns an error directing user to migrate. | |||
| 159 | + | func (m *Manager) checkLegacyFormat() error { | |||
| 160 | + | // Check if old sections exist by looking for known keys. | |||
| 161 | + | // IsSet returns true only for explicitly set values (not SetDefault), so this | |||
| 162 | + | // correctly identifies keys present in the config file, env vars, or overrides. | |||
| 163 | + | hasOldSections := m.viper.IsSet("rendering.mode") || | |||
| 164 | + | m.viper.IsSet("rendering.disable_dmabuf_renderer") || | |||
| 165 | + | m.viper.IsSet("performance.profile") || | |||
| 166 | + | m.viper.IsSet("privacy.cookie_policy") || | |||
| 167 | + | m.viper.IsSet("runtime.prefix") | |||
| 168 | + | ||||
| 169 | + | hasEngineSection := m.viper.IsSet("engine.type") | |||
| 170 | + | ||||
| 171 | + | if hasOldSections && !hasEngineSection { | |||
| 172 | + | return fmt.Errorf( | |||
| 173 | + | "config format outdated: [rendering], [performance], [privacy] sections " + | |||
| 174 | + | "have moved to [engine]/[engine.webkit].\n" + | |||
| 175 | + | "Run \"dumber migrate\" to update your config file.", | |||
| 176 | + | ) | |||
| 177 | + | } | |||
| 178 | + | return nil | |||
| 179 | + | } | |||
| 180 | + | ||||
| 154 | func (m *Manager) unmarshalConfig() (*Config, error) { | 181 | func (m *Manager) unmarshalConfig() (*Config, error) { | ||
| 155 | config := &Config{} | 182 | config := &Config{} | ||
| 156 | if err := m.viper.Unmarshal(config); err != nil { | 183 | if err := m.viper.Unmarshal(config); err != nil { | ||
A internal/infrastructure/config/loader_legacy_test.go +76 -0
| @@ -0,0 +1,76 @@ | |||
| 1 | + | package config | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/spf13/viper" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestCheckLegacyFormat_OldSectionsNoEngine_ReturnsError(t *testing.T) { | |
| 11 | + | m := &Manager{viper: viper.New()} | |
| 12 | + | m.viper.Set("rendering.mode", "gpu") | |
| 13 | + | ||
| 14 | + | err := m.checkLegacyFormat() | |
| 15 | + | ||
| 16 | + | require.Error(t, err) | |
| 17 | + | require.Contains(t, err.Error(), "dumber migrate") | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func TestCheckLegacyFormat_OldSectionsWithEngine_ReturnsNoError(t *testing.T) { | |
| 21 | + | m := &Manager{viper: viper.New()} | |
| 22 | + | m.viper.Set("rendering.mode", "gpu") | |
| 23 | + | m.viper.Set("engine.type", "webkit") | |
| 24 | + | ||
| 25 | + | err := m.checkLegacyFormat() | |
| 26 | + | ||
| 27 | + | require.NoError(t, err) | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func TestCheckLegacyFormat_FreshConfig_ReturnsNoError(t *testing.T) { | |
| 31 | + | m := &Manager{viper: viper.New()} | |
| 32 | + | ||
| 33 | + | err := m.checkLegacyFormat() | |
| 34 | + | ||
| 35 | + | require.NoError(t, err) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func TestCheckLegacyFormat_DisableDmabuf_ReturnsError(t *testing.T) { | |
| 39 | + | m := &Manager{viper: viper.New()} | |
| 40 | + | m.viper.Set("rendering.disable_dmabuf_renderer", true) | |
| 41 | + | ||
| 42 | + | err := m.checkLegacyFormat() | |
| 43 | + | ||
| 44 | + | require.Error(t, err) | |
| 45 | + | require.Contains(t, err.Error(), "dumber migrate") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func TestCheckLegacyFormat_PerformanceProfile_ReturnsError(t *testing.T) { | |
| 49 | + | m := &Manager{viper: viper.New()} | |
| 50 | + | m.viper.Set("performance.profile", "lite") | |
| 51 | + | ||
| 52 | + | err := m.checkLegacyFormat() | |
| 53 | + | ||
| 54 | + | require.Error(t, err) | |
| 55 | + | require.Contains(t, err.Error(), "dumber migrate") | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func TestCheckLegacyFormat_PrivacyCookiePolicy_ReturnsError(t *testing.T) { | |
| 59 | + | m := &Manager{viper: viper.New()} | |
| 60 | + | m.viper.Set("privacy.cookie_policy", "always") | |
| 61 | + | ||
| 62 | + | err := m.checkLegacyFormat() | |
| 63 | + | ||
| 64 | + | require.Error(t, err) | |
| 65 | + | require.Contains(t, err.Error(), "dumber migrate") | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func TestCheckLegacyFormat_RuntimePrefix_ReturnsError(t *testing.T) { | |
| 69 | + | m := &Manager{viper: viper.New()} | |
| 70 | + | m.viper.Set("runtime.prefix", "/usr") | |
| 71 | + | ||
| 72 | + | err := m.checkLegacyFormat() | |
| 73 | + | ||
| 74 | + | require.Error(t, err) | |
| 75 | + | require.Contains(t, err.Error(), "dumber migrate") | |
| 76 | + | } | |
| @@ -0,0 +1,76 @@ | |||||
| 1 | + | package config | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/spf13/viper" | |||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestCheckLegacyFormat_OldSectionsNoEngine_ReturnsError(t *testing.T) { | |||
| 11 | + | m := &Manager{viper: viper.New()} | |||
| 12 | + | m.viper.Set("rendering.mode", "gpu") | |||
| 13 | + | ||||
| 14 | + | err := m.checkLegacyFormat() | |||
| 15 | + | ||||
| 16 | + | require.Error(t, err) | |||
| 17 | + | require.Contains(t, err.Error(), "dumber migrate") | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func TestCheckLegacyFormat_OldSectionsWithEngine_ReturnsNoError(t *testing.T) { | |||
| 21 | + | m := &Manager{viper: viper.New()} | |||
| 22 | + | m.viper.Set("rendering.mode", "gpu") | |||
| 23 | + | m.viper.Set("engine.type", "webkit") | |||
| 24 | + | ||||
| 25 | + | err := m.checkLegacyFormat() | |||
| 26 | + | ||||
| 27 | + | require.NoError(t, err) | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | func TestCheckLegacyFormat_FreshConfig_ReturnsNoError(t *testing.T) { | |||
| 31 | + | m := &Manager{viper: viper.New()} | |||
| 32 | + | ||||
| 33 | + | err := m.checkLegacyFormat() | |||
| 34 | + | ||||
| 35 | + | require.NoError(t, err) | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func TestCheckLegacyFormat_DisableDmabuf_ReturnsError(t *testing.T) { | |||
| 39 | + | m := &Manager{viper: viper.New()} | |||
| 40 | + | m.viper.Set("rendering.disable_dmabuf_renderer", true) | |||
| 41 | + | ||||
| 42 | + | err := m.checkLegacyFormat() | |||
| 43 | + | ||||
| 44 | + | require.Error(t, err) | |||
| 45 | + | require.Contains(t, err.Error(), "dumber migrate") | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func TestCheckLegacyFormat_PerformanceProfile_ReturnsError(t *testing.T) { | |||
| 49 | + | m := &Manager{viper: viper.New()} | |||
| 50 | + | m.viper.Set("performance.profile", "lite") | |||
| 51 | + | ||||
| 52 | + | err := m.checkLegacyFormat() | |||
| 53 | + | ||||
| 54 | + | require.Error(t, err) | |||
| 55 | + | require.Contains(t, err.Error(), "dumber migrate") | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func TestCheckLegacyFormat_PrivacyCookiePolicy_ReturnsError(t *testing.T) { | |||
| 59 | + | m := &Manager{viper: viper.New()} | |||
| 60 | + | m.viper.Set("privacy.cookie_policy", "always") | |||
| 61 | + | ||||
| 62 | + | err := m.checkLegacyFormat() | |||
| 63 | + | ||||
| 64 | + | require.Error(t, err) | |||
| 65 | + | require.Contains(t, err.Error(), "dumber migrate") | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func TestCheckLegacyFormat_RuntimePrefix_ReturnsError(t *testing.T) { | |||
| 69 | + | m := &Manager{viper: viper.New()} | |||
| 70 | + | m.viper.Set("runtime.prefix", "/usr") | |||
| 71 | + | ||||
| 72 | + | err := m.checkLegacyFormat() | |||
| 73 | + | ||||
| 74 | + | require.Error(t, err) | |||
| 75 | + | require.Contains(t, err.Error(), "dumber migrate") | |||
| 76 | + | } | |||