detached 005fae9

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
8787 return err
8888 }
8989
90+ if err := m.checkLegacyFormat(); err != nil {
91+ return err
92+ }
93+
9094 config, err := m.unmarshalConfig()
9195 if err != nil {
9296 return err
@@ -151,6 +155,29 @@ func (m *Manager) transformLegacyActionBindings() {
151155 }
152156 }
153157
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+
154181 func (m *Manager) unmarshalConfig() (*Config, error) {
155182 config := &Config{}
156183 if err := m.viper.Unmarshal(config); err != nil {
@@ -87,6 +87,10 @@ func (m *Manager) Load() error {
87 return err87 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 err96 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close