1package config
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "github.com/spf13/viper"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14// --- Legacy popups → browsing_contexts transform tests ---
15
16func TestTransformLegacyPopupsToBrowsingContexts_BasicMapping(t *testing.T) {
17 transformer := NewLegacyConfigTransformer()
18
19 rawConfig := map[string]any{
20 "workspace": map[string]any{
21 "popups": map[string]any{
22 "behavior": "tabbed",
23 "oauth_auto_close": false,
24 },
25 },
26 }
27
28 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
29
30 ws := rawConfig["workspace"].(map[string]any)
31 // Old key should be deleted.
32 _, hasOld := ws["popups"]
33 assert.False(t, hasOld, "popups key should be removed after transform")
34
35 // New key should contain the old values.
36 newCfg, hasNew := ws["browsing_contexts"].(map[string]any)
37 require.True(t, hasNew, "browsing_contexts key should exist")
38 assert.Equal(t, "tabbed", newCfg["behavior"])
39 assert.Equal(t, false, newCfg["oauth_auto_close"])
40}
41
42func TestTransformLegacyPopupsToBrowsingContexts_NewKeyAlreadyPresent(t *testing.T) {
43 transformer := NewLegacyConfigTransformer()
44
45 rawConfig := map[string]any{
46 "workspace": map[string]any{
47 "popups": map[string]any{
48 "behavior": "tabbed",
49 },
50 "browsing_contexts": map[string]any{
51 "behavior": "split",
52 },
53 },
54 }
55
56 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
57
58 ws := rawConfig["workspace"].(map[string]any)
59 // Old key should still exist (new key was already present, skip transform).
60 oldCfg, hasOld := ws["popups"].(map[string]any)
61 assert.True(t, hasOld, "popups key should remain when browsing_contexts already exists")
62 assert.Equal(t, "tabbed", oldCfg["behavior"])
63
64 // New key should NOT be overwritten.
65 newCfg, hasNew := ws["browsing_contexts"].(map[string]any)
66 require.True(t, hasNew, "browsing_contexts key should still exist")
67 assert.Equal(t, "split", newCfg["behavior"], "browsing_contexts should not be overwritten by legacy transform")
68}
69
70func TestTransformLegacyPopupsToBrowsingContexts_NoWorkspace(t *testing.T) {
71 transformer := NewLegacyConfigTransformer()
72 rawConfig := map[string]any{"other": "value"}
73 // Should not panic
74 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
75 assert.NotNil(t, rawConfig)
76}
77
78func TestTransformLegacyPopupsToBrowsingContexts_NoPopupsKey(t *testing.T) {
79 transformer := NewLegacyConfigTransformer()
80 rawConfig := map[string]any{
81 "workspace": map[string]any{
82 "new_pane_url": "about:blank",
83 },
84 }
85 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
86 ws := rawConfig["workspace"].(map[string]any)
87 assert.Equal(t, "about:blank", ws["new_pane_url"])
88 _, hasBC := ws["browsing_contexts"]
89 assert.False(t, hasBC, "browsing_contexts should not be created if popups absent")
90}
91
92// --- Default config writing emits browsing_contexts instead of popups ---
93
94func TestDefaultConfig_TOMLHasBrowsingContextsNotPopups(t *testing.T) {
95 cfg := DefaultConfig()
96
97 content, err := encodeConfigToTOML(cfg)
98 require.NoError(t, err)
99
100 // Should contain [workspace.browsing_contexts] section.
101 assert.Contains(t, content, "[workspace.browsing_contexts]",
102 "TOML output should contain browsing_contexts section")
103
104 // Should NOT contain [workspace.popups] section.
105 assert.NotContains(t, content, "[workspace.popups]",
106 "TOML output should NOT contain deprecated popups section")
107
108 // The section should have the expected keys.
109 assert.Contains(t, content, "behavior = ")
110 assert.Contains(t, content, "oauth_auto_close = ")
111}
112
113// --- Loader defaults expose browsing_contexts keys, NOT popups ---
114
115func TestSetWorkspaceDefaults_ExposesBrowsingContextsNotPopups(t *testing.T) {
116 mgr := &Manager{viper: viper.New()}
117 mgr.setDefaults()
118
119 // browsing_contexts defaults should be registered.
120 assert.Equal(t, "split", mgr.viper.GetString("workspace.browsing_contexts.behavior"))
121 assert.Equal(t, "right", mgr.viper.GetString("workspace.browsing_contexts.placement"))
122 assert.Equal(t, "stacked", mgr.viper.GetString("workspace.browsing_contexts.blank_target_behavior"))
123 assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.open_in_new_pane"))
124 assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.follow_pane_context"))
125 assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.enable_smart_detection"))
126 assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.oauth_auto_close"))
127
128 // popups defaults should NOT be registered.
129 assert.Empty(t, mgr.viper.GetString("workspace.popups.behavior"))
130 assert.Empty(t, mgr.viper.GetString("workspace.popups.placement"))
131 assert.Empty(t, mgr.viper.GetString("workspace.popups.blank_target_behavior"))
132 assert.False(t, mgr.viper.GetBool("workspace.popups.open_in_new_pane"))
133 assert.False(t, mgr.viper.GetBool("workspace.popups.follow_pane_context"))
134 assert.False(t, mgr.viper.GetBool("workspace.popups.enable_smart_detection"))
135 assert.False(t, mgr.viper.GetBool("workspace.popups.oauth_auto_close"))
136}
137
138func TestBindBrowsingContextEnvAliases_AcceptsLegacyPopupEnvNames(t *testing.T) {
139 t.Setenv("DUMBER_WORKSPACE_POPUPS_BEHAVIOR", "tabbed")
140
141 v := viper.New()
142 v.SetEnvPrefix("DUMBER")
143 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
144 v.AutomaticEnv()
145 v.SetDefault("workspace.browsing_contexts.behavior", "split")
146 require.NoError(t, bindBrowsingContextEnvAliases(v))
147
148 assert.Equal(t, "tabbed", v.GetString("workspace.browsing_contexts.behavior"))
149}
150
151// --- Validation error paths mention workspace.browsing_contexts, not workspace.popups ---
152
153func TestValidatePopups_ErrorMessagesUseBrowsingContexts(t *testing.T) {
154 tests := []struct {
155 name string
156 mutate func(*Config)
157 wantText string
158 }{
159 {
160 name: "invalid behavior",
161 mutate: func(cfg *Config) {
162 cfg.Workspace.BrowsingContexts.Behavior = PopupBehavior("bad")
163 },
164 wantText: "workspace.browsing_contexts.behavior",
165 },
166 {
167 name: "invalid placement",
168 mutate: func(cfg *Config) {
169 cfg.Workspace.BrowsingContexts.Behavior = PopupBehaviorSplit
170 cfg.Workspace.BrowsingContexts.Placement = "center"
171 },
172 wantText: "workspace.browsing_contexts.placement",
173 },
174 {
175 name: "invalid blank_target_behavior",
176 mutate: func(cfg *Config) {
177 cfg.Workspace.BrowsingContexts.BlankTargetBehavior = "popup"
178 },
179 wantText: "workspace.browsing_contexts.blank_target_behavior",
180 },
181 }
182
183 for _, tt := range tests {
184 t.Run(tt.name, func(t *testing.T) {
185 cfg := DefaultConfig()
186 // Sync Popups alias so both fields match (validation reads BrowsingContexts).
187 cfg.Workspace.Popups = cfg.Workspace.BrowsingContexts
188 tt.mutate(cfg)
189 cfg.Workspace.Popups = cfg.Workspace.BrowsingContexts
190
191 err := validateConfig(cfg)
192 require.Error(t, err)
193 assert.Contains(t, err.Error(), tt.wantText,
194 "validation error should reference browsing_contexts, not popups")
195
196 // Ensure it does NOT reference the old popups nomenclature.
197 assert.NotContains(t, err.Error(), "workspace.popups.",
198 "validation error should NOT reference deprecated workspace.popups")
199 })
200 }
201}
202
203func TestValidatePopups_CompatibilityAliasMirrorsBrowsingContexts(t *testing.T) {
204 // When BrowsingContexts is valid but Popups alias is stale,
205 // validation should still pass because it reads BrowsingContexts.
206 cfg := DefaultConfig()
207 // Deliberately make Popups stale.
208 cfg.Workspace.Popups.Behavior = PopupBehavior("invalid")
209 cfg.Workspace.Popups.OAuthAutoClose = !cfg.Workspace.BrowsingContexts.OAuthAutoClose
210
211 err := validateConfig(cfg)
212 require.NoError(t, err, "validation should read BrowsingContexts, not the stale Popups alias")
213}
214
215// --- Runtime alias sync (BrowsingContexts → Popups) ---
216
217func TestNormalizeBrowsingContexts_SyncsPopupsAlias(t *testing.T) {
218 cfg := DefaultConfig()
219
220 // Make BrowsingContexts and Popups diverge.
221 cfg.Workspace.BrowsingContexts.Behavior = PopupBehaviorTabbed
222 cfg.Workspace.Popups.Behavior = PopupBehaviorSplit
223
224 normalizeBrowsingContexts(cfg)
225
226 assert.Equal(t, PopupBehaviorTabbed, cfg.Workspace.Popups.Behavior,
227 "normalizeConfig should sync Popups ← BrowsingContexts")
228 assert.Equal(t, cfg.Workspace.BrowsingContexts.Placement, cfg.Workspace.Popups.Placement)
229 assert.Equal(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose, cfg.Workspace.Popups.OAuthAutoClose)
230 assert.Equal(t, cfg.Workspace.BrowsingContexts.OpenInNewPane, cfg.Workspace.Popups.OpenInNewPane)
231}
232
233func TestDefaultConfig_PopupsMirrorsBrowsingContexts(t *testing.T) {
234 cfg := DefaultConfig()
235
236 // After DefaultConfig(), both fields should be identical.
237 assert.Equal(t, cfg.Workspace.BrowsingContexts.Behavior, cfg.Workspace.Popups.Behavior)
238 assert.Equal(t, cfg.Workspace.BrowsingContexts.Placement, cfg.Workspace.Popups.Placement)
239 assert.Equal(t, cfg.Workspace.BrowsingContexts.BlankTargetBehavior, cfg.Workspace.Popups.BlankTargetBehavior)
240 assert.Equal(t, cfg.Workspace.BrowsingContexts.OpenInNewPane, cfg.Workspace.Popups.OpenInNewPane)
241 assert.Equal(t, cfg.Workspace.BrowsingContexts.FollowPaneContext, cfg.Workspace.Popups.FollowPaneContext)
242 assert.Equal(t, cfg.Workspace.BrowsingContexts.EnableSmartDetection, cfg.Workspace.Popups.EnableSmartDetection)
243 assert.Equal(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose, cfg.Workspace.Popups.OAuthAutoClose)
244}
245
246func TestSchemaProvider_UsesBrowsingContextsKeys(t *testing.T) {
247 provider := NewSchemaProvider()
248 schema := provider.GetSchema()
249
250 foundBrowsingContexts := false
251 foundPopups := false
252
253 for _, key := range schema {
254 if key.Key == "workspace.browsing_contexts.behavior" {
255 foundBrowsingContexts = true
256 }
257 if key.Key == "workspace.popups.behavior" {
258 foundPopups = true
259 }
260 }
261
262 assert.True(t, foundBrowsingContexts, "schema should include workspace.browsing_contexts.* keys")
263 assert.False(t, foundPopups, "schema should NOT include workspace.popups.* keys")
264}
265
266// --- End-to-end: legacy config round-trips through viper ---
267
268func TestLegacyPopupsFullRoundTrip(t *testing.T) {
269 // Simulate reading an old config file with [workspace.popups].
270 m := &Manager{viper: viper.New()}
271 m.viper.Set("workspace.popups.behavior", "stacked")
272 m.viper.Set("workspace.popups.placement", "left")
273 m.viper.Set("workspace.popups.oauth_auto_close", false)
274
275 // Transform should map popups → browsing_contexts.
276 settings, err := m.transformedSettings()
277 require.NoError(t, err)
278
279 // popups key should be gone from viper after transform.
280 assert.False(t, settings.IsSet("workspace.popups.behavior"),
281 "workspace.popups.behavior should not exist after transform")
282
283 // browsing_contexts should now hold the values.
284 assert.Equal(t, "stacked", settings.GetString("workspace.browsing_contexts.behavior"))
285 assert.Equal(t, "left", settings.GetString("workspace.browsing_contexts.placement"))
286 assert.False(t, settings.GetBool("workspace.browsing_contexts.oauth_auto_close"))
287
288 // Unmarshal should succeed.
289 cfg := &Config{}
290 err = settings.Unmarshal(cfg)
291 require.NoError(t, err)
292
293 // BrowsingContexts should have the legacy values.
294 assert.Equal(t, PopupBehaviorStacked, cfg.Workspace.BrowsingContexts.Behavior)
295 assert.Equal(t, "left", cfg.Workspace.BrowsingContexts.Placement)
296 assert.False(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose)
297
298 // Popups should be empty (not unmarshaled, will be synced in normalizeConfig).
299 assert.Equal(t, PopupBehavior(""), cfg.Workspace.Popups.Behavior)
300
301 // After normalization, Popups should mirror BrowsingContexts.
302 normalizeBrowsingContexts(cfg)
303 assert.Equal(t, PopupBehaviorStacked, cfg.Workspace.Popups.Behavior)
304 assert.Equal(t, "left", cfg.Workspace.Popups.Placement)
305 assert.False(t, cfg.Workspace.Popups.OAuthAutoClose)
306}
307
308func TestLegacyPopupsFileOverridesBrowsingContextDefaults(t *testing.T) {
309 configFile := filepath.Join(t.TempDir(), "config.toml")
310 content := `
311[workspace.popups]
312behavior = "stacked"
313placement = "left"
314oauth_auto_close = false
315`
316 require.NoError(t, os.WriteFile(configFile, []byte(content), 0o644))
317
318 m := &Manager{viper: viper.New()}
319 m.viper.SetConfigFile(configFile)
320 m.setDefaults()
321 require.NoError(t, m.viper.ReadInConfig())
322
323 settings, err := m.transformedSettings()
324 require.NoError(t, err)
325
326 assert.Equal(t, "stacked", settings.GetString("workspace.browsing_contexts.behavior"))
327 assert.Equal(t, "left", settings.GetString("workspace.browsing_contexts.placement"))
328 assert.False(t, settings.GetBool("workspace.browsing_contexts.oauth_auto_close"))
329
330 cfg := &Config{}
331 require.NoError(t, settings.Unmarshal(cfg))
332 normalizeConfig(cfg)
333
334 assert.Equal(t, PopupBehaviorStacked, cfg.Workspace.BrowsingContexts.Behavior)
335 assert.Equal(t, "left", cfg.Workspace.BrowsingContexts.Placement)
336 assert.False(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose)
337 assert.Equal(t, cfg.Workspace.BrowsingContexts, cfg.Workspace.Popups)
338}
339
340func TestLegacyPopupsFilePreservesCanonicalBrowsingContextsEnvOverride(t *testing.T) {
341 configFile := filepath.Join(t.TempDir(), "config.toml")
342 content := `
343[workspace.popups]
344behavior = "stacked"
345placement = "left"
346`
347 require.NoError(t, os.WriteFile(configFile, []byte(content), 0o644))
348 t.Setenv("DUMBER_WORKSPACE_BROWSING_CONTEXTS_BEHAVIOR", "tabbed")
349
350 m := &Manager{viper: viper.New()}
351 m.viper.SetConfigType("toml")
352 m.viper.SetConfigFile(configFile)
353 m.viper.SetEnvPrefix("DUMBER")
354 m.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
355 m.viper.AutomaticEnv()
356 m.setDefaults()
357 require.NoError(t, m.viper.ReadInConfig())
358
359 settings, err := m.transformedSettings()
360 require.NoError(t, err)
361
362 assert.Equal(t, "tabbed", settings.GetString("workspace.browsing_contexts.behavior"))
363 assert.Equal(t, "left", settings.GetString("workspace.browsing_contexts.placement"))
364}
365
366func TestReload_ReappliesLegacyPopupsCompatibility(t *testing.T) {
367 configFile := filepath.Join(t.TempDir(), "config.toml")
368 writeConfig := func(content string) {
369 require.NoError(t, os.WriteFile(configFile, []byte(content), 0o644))
370 }
371
372 m := &Manager{viper: viper.New()}
373 m.viper.SetConfigType("toml")
374 m.viper.SetConfigFile(configFile)
375 m.viper.SetEnvPrefix("DUMBER")
376 m.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
377 m.viper.AutomaticEnv()
378 m.setDefaults()
379
380 writeConfig(`
381[workspace.popups]
382behavior = "stacked"
383placement = "left"
384`)
385 require.NoError(t, m.reload())
386 require.Equal(t, PopupBehaviorStacked, m.config.Workspace.BrowsingContexts.Behavior)
387 require.Equal(t, "left", m.config.Workspace.BrowsingContexts.Placement)
388
389 writeConfig(`
390[workspace.popups]
391behavior = "tabbed"
392placement = "bottom"
393`)
394 require.NoError(t, m.reload())
395 assert.Equal(t, PopupBehaviorTabbed, m.config.Workspace.BrowsingContexts.Behavior)
396 assert.Equal(t, "bottom", m.config.Workspace.BrowsingContexts.Placement)
397 assert.Equal(t, m.config.Workspace.BrowsingContexts, m.config.Workspace.Popups)
398}
399
400func TestReload_MixedLegacyAndCanonicalBrowsingContexts_BackfillsMissingCanonicalFields(t *testing.T) {
401 configFile := filepath.Join(t.TempDir(), "config.toml")
402 require.NoError(t, os.WriteFile(configFile, []byte(`
403[workspace.popups]
404placement = "left"
405oauth_auto_close = false
406
407[workspace.browsing_contexts]
408behavior = "tabbed"
409`), 0o644))
410
411 m := &Manager{viper: viper.New()}
412 m.viper.SetConfigType("toml")
413 m.viper.SetConfigFile(configFile)
414 m.viper.SetEnvPrefix("DUMBER")
415 m.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
416 m.viper.AutomaticEnv()
417 m.setDefaults()
418
419 require.NoError(t, m.reload())
420 assert.Equal(t, PopupBehaviorTabbed, m.config.Workspace.BrowsingContexts.Behavior)
421 assert.Equal(t, "left", m.config.Workspace.BrowsingContexts.Placement)
422 assert.False(t, m.config.Workspace.BrowsingContexts.OAuthAutoClose)
423 assert.Equal(t, m.config.Workspace.BrowsingContexts, m.config.Workspace.Popups)
424}
425
426func TestSave_WhenWatching_SyncsPopupsAliasBeforeSkipReload(t *testing.T) {
427 configFile := filepath.Join(t.TempDir(), "config.toml")
428 m := &Manager{viper: viper.New(), watching: true}
429 m.viper.SetConfigType("toml")
430 m.viper.SetConfigFile(configFile)
431 m.setDefaults()
432 m.config = DefaultConfig()
433
434 cfg := DefaultConfig()
435 cfg.Workspace.BrowsingContexts.Behavior = PopupBehaviorTabbed
436 cfg.Workspace.BrowsingContexts.Placement = "bottom"
437 cfg.Workspace.Popups.Behavior = PopupBehaviorSplit
438 cfg.Workspace.Popups.Placement = "left"
439
440 require.NoError(t, m.Save(cfg))
441 assert.True(t, m.skipNextReload)
442 assert.Equal(t, cfg.Workspace.BrowsingContexts.Behavior, m.config.Workspace.BrowsingContexts.Behavior)
443 assert.Equal(t, cfg.Workspace.BrowsingContexts.Placement, m.config.Workspace.BrowsingContexts.Placement)
444 assert.Equal(t, m.config.Workspace.BrowsingContexts, m.config.Workspace.Popups)
445}