package config import ( "math" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestValidateConfig_ModeLegend(t *testing.T) { for _, tc := range []struct { name, mode string delay int wantError string }{ {name: "default", mode: "delay", delay: 500}, {name: "always", mode: "always"}, {name: "off", mode: "off"}, {name: "unknown mode", mode: "sometimes", wantError: "workspace.styling.mode_legend must be always, delay, or off"}, {name: "negative delay", mode: "delay", delay: -1, wantError: "workspace.styling.mode_legend_delay_ms must be non-negative"}, } { t.Run(tc.name, func(t *testing.T) { cfg := DefaultConfig() cfg.Workspace.Styling.ModeLegend = tc.mode cfg.Workspace.Styling.ModeLegendDelayMs = tc.delay err := validateConfig(cfg) if tc.wantError != "" { require.EqualError(t, err, "config validation failed:\n - "+tc.wantError) } else { require.NoError(t, err) } }) } } func TestValidateConfig_OmniboxStyle(t *testing.T) { tests := []struct { style string wantErr bool }{ {style: "command"}, {style: "multiplexer"}, {style: "minimal"}, {style: "", wantErr: true}, {style: "classic", wantErr: true}, } for _, tt := range tests { t.Run(tt.style, func(t *testing.T) { cfg := DefaultConfig() cfg.Omnibox.Style = tt.style err := validateConfig(cfg) if tt.wantErr { require.Error(t, err) assert.Contains(t, err.Error(), "omnibox.style") return } require.NoError(t, err) }) } } func TestValidateConfig_EngineType(t *testing.T) { t.Setenv("DUMBER_ENGINE", "") tests := []struct { name string engineType string wantErr bool }{ {name: "cef", engineType: EngineTypeCEF, wantErr: false}, {name: "webkit", engineType: EngineTypeWebKit, wantErr: false}, {name: "empty is invalid", engineType: "", wantErr: true}, {name: "invalid", engineType: "netscape", wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := DefaultConfig() cfg.Engine.Type = tt.engineType err := validateConfig(cfg) if tt.wantErr { require.Error(t, err) assert.Contains(t, err.Error(), "engine.type") return } require.NoError(t, err) }) } } func TestValidateConfig_EngineTypeIgnoresEnvOverride(t *testing.T) { t.Run("invalid configured type is still rejected when env override is valid", func(t *testing.T) { t.Setenv("DUMBER_ENGINE", EngineTypeWebKit) cfg := DefaultConfig() cfg.Engine.Type = "netscape" err := validateConfig(cfg) require.Error(t, err) assert.Contains(t, err.Error(), "engine.type") }) t.Run("valid configured type is not rejected when env override is invalid", func(t *testing.T) { t.Setenv("DUMBER_ENGINE", "netscape") cfg := DefaultConfig() cfg.Engine.Type = EngineTypeCEF err := validateConfig(cfg) require.NoError(t, err) }) } func TestValidateConfig_EngineCookiePolicy(t *testing.T) { tests := []struct { name string cookiePolicy CookiePolicy wantErr bool }{ {name: "always", cookiePolicy: CookiePolicyAlways, wantErr: false}, {name: "no_third_party", cookiePolicy: CookiePolicyNoThirdParty, wantErr: false}, {name: "never", cookiePolicy: CookiePolicyNever, wantErr: false}, {name: "invalid", cookiePolicy: CookiePolicy("bad_policy"), wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := DefaultConfig() cfg.Engine.CookiePolicy = tt.cookiePolicy err := validateConfig(cfg) if tt.wantErr { require.Error(t, err) assert.Contains(t, err.Error(), "engine.cookie_policy") return } require.NoError(t, err) }) } } func TestValidateConfig_WebKitMediaSettings(t *testing.T) { tests := []struct { name string mutate func(*Config) wantErr bool wantField string }{ { name: "valid gl rendering modes", mutate: func(cfg *Config) { cfg.Engine.WebKit.GLRenderingMode = GLRenderingModeGLES2 }, wantErr: false, }, { name: "invalid gl rendering mode", mutate: func(cfg *Config) { cfg.Engine.WebKit.GLRenderingMode = GLRenderingMode("metal") }, wantErr: true, wantField: "engine.webkit.gl_rendering_mode", }, { name: "gstreamer debug level lower bound", mutate: func(cfg *Config) { cfg.Engine.WebKit.GStreamerDebugLevel = 0 }, wantErr: false, }, { name: "gstreamer debug level upper bound", mutate: func(cfg *Config) { cfg.Engine.WebKit.GStreamerDebugLevel = 5 }, wantErr: false, }, { name: "negative gstreamer debug level", mutate: func(cfg *Config) { cfg.Engine.WebKit.GStreamerDebugLevel = -1 }, wantErr: true, wantField: "engine.webkit.gstreamer_debug_level", }, { name: "too high gstreamer debug level", mutate: func(cfg *Config) { cfg.Engine.WebKit.GStreamerDebugLevel = 6 }, wantErr: true, wantField: "engine.webkit.gstreamer_debug_level", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := DefaultConfig() tt.mutate(cfg) err := validateConfig(cfg) if tt.wantErr { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantField) return } require.NoError(t, err) }) } } func TestValidateConfig_CEFConfig(t *testing.T) { tests := []struct { name string mutate func(*Config) wantErr bool wantText string }{ { name: "valid defaults", mutate: func(_ *Config) { }, wantErr: false, }, { name: "invalid log severity", mutate: func(cfg *Config) { cfg.Engine.CEF.LogSeverity = 7 }, wantErr: true, wantText: "engine.cef.log_severity", }, { name: "zero frame rate uses default", mutate: func(cfg *Config) { cfg.Engine.CEF.WindowlessFrameRate = 0 }, wantErr: false, }, { name: "negative frame rate", mutate: func(cfg *Config) { cfg.Engine.CEF.WindowlessFrameRate = -1 }, wantErr: true, wantText: "engine.cef.windowless_frame_rate", }, { name: "zero idle residency timeout disables", mutate: func(cfg *Config) { cfg.Engine.CEF.IdleRuntimeTimeoutMs = 0 }, wantErr: false, }, { name: "maximum idle residency timeout accepted", mutate: func(cfg *Config) { cfg.Engine.CEF.IdleRuntimeTimeoutMs = 300000 }, wantErr: false, }, { name: "negative idle residency timeout", mutate: func(cfg *Config) { cfg.Engine.CEF.IdleRuntimeTimeoutMs = -1 }, wantErr: true, wantText: "engine.cef.idle_runtime_timeout_ms", }, { name: "oversized idle residency timeout", mutate: func(cfg *Config) { cfg.Engine.CEF.IdleRuntimeTimeoutMs = 300001 }, wantErr: true, wantText: "engine.cef.idle_runtime_timeout_ms", }, { name: "zero wheel multiplier", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollWheelMultiplier = 0 }, wantErr: true, wantText: "engine.cef.input.scroll_wheel_multiplier", }, { name: "negative precise multiplier", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollPreciseMultiplier = -0.1 }, wantErr: true, wantText: "engine.cef.input.scroll_precise_multiplier", }, { name: "zero horizontal multiplier", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = 0 }, wantErr: true, wantText: "engine.cef.input.scroll_horizontal_multiplier", }, { name: "negative vertical multiplier", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollVerticalMultiplier = -0.1 }, wantErr: true, wantText: "engine.cef.input.scroll_vertical_multiplier", }, { name: "nan horizontal multiplier", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = math.NaN() }, wantErr: true, wantText: "engine.cef.input.scroll_horizontal_multiplier", }, { name: "infinite vertical multiplier", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollVerticalMultiplier = math.Inf(1) }, wantErr: true, wantText: "engine.cef.input.scroll_vertical_multiplier", }, { name: "zero max delta disables clamp", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollMaxDelta = 0 }, wantErr: false, }, { name: "negative max delta", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.ScrollMaxDelta = -1 }, wantErr: true, wantText: "engine.cef.input.scroll_max_delta", }, { name: "zero touchpad navigation min delta", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.TouchpadNavigationMinDelta = 0 }, wantErr: true, wantText: "engine.cef.input.touchpad_navigation_min_delta", }, { name: "nan touchpad navigation max vertical ratio", mutate: func(cfg *Config) { cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio = math.NaN() }, wantErr: true, wantText: "engine.cef.input.touchpad_navigation_max_vertical_ratio", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := DefaultConfig() tt.mutate(cfg) err := validateConfig(cfg) if tt.wantErr { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantText) return } require.NoError(t, err) }) } } func TestValidateConfig_ExternalLinksAcceptsSupportedValues(t *testing.T) { for _, behavior := range []ExternalLinkBehavior{ ExternalLinkBehaviorWindowed, ExternalLinkBehaviorTabbed, ExternalLinkBehaviorSplit, ExternalLinkBehaviorStacked, } { for _, placement := range []ExternalLinkPlacement{ ExternalLinkPlacementRight, ExternalLinkPlacementLeft, ExternalLinkPlacementTop, ExternalLinkPlacementBottom, } { cfg := DefaultConfig() cfg.Workspace.ExternalLinks.Behavior = behavior cfg.Workspace.ExternalLinks.Placement = placement require.NoError(t, validateConfig(cfg)) } } } func TestValidateConfig_ExternalLinksRejectsUnsupportedValues(t *testing.T) { cfg := DefaultConfig() cfg.Workspace.ExternalLinks.Behavior = "invalid" cfg.Workspace.ExternalLinks.Placement = "invalid" err := validateConfig(cfg) require.Error(t, err) assert.Contains(t, err.Error(), "workspace.external_links.behavior") assert.Contains(t, err.Error(), "workspace.external_links.placement") } func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingAbsoluteLocalPath(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "page.html") require.NoError(t, os.WriteFile(path, []byte("ok"), 0644)) cfg := DefaultConfig() cfg.Workspace.NewPaneURL = path require.NoError(t, validateConfig(cfg)) } func TestValidateConfig_WorkspaceNewPaneURLRejectsRelativeLocalPathLikeValues(t *testing.T) { // This test mutates process CWD; do not add t.Parallel here. tmpDir := t.TempDir() oldWD, err := os.Getwd() require.NoError(t, err) childDir := filepath.Join(tmpDir, "child") require.NoError(t, os.Mkdir(childDir, 0755)) require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "page.html"), []byte("ok"), 0644)) require.NoError(t, os.WriteFile(filepath.Join(childDir, "page.html"), []byte("ok"), 0644)) require.NoError(t, os.Chdir(childDir)) t.Cleanup(func() { _ = os.Chdir(oldWD) }) for _, value := range []string{"./page.html", "../page.html"} { t.Run(value, func(t *testing.T) { cfg := DefaultConfig() cfg.Workspace.NewPaneURL = value err := validateConfig(cfg) require.Error(t, err) assert.Contains(t, err.Error(), "workspace.new_pane_url relative local path cannot be resolved") }) } } func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *testing.T) { for _, value := range []string{"/missing/dumber/page.html", "~/missing-dumber-page.html"} { t.Run(value, func(t *testing.T) { cfg := DefaultConfig() cfg.Workspace.NewPaneURL = value err := validateConfig(cfg) require.Error(t, err) assert.Contains(t, err.Error(), "workspace.new_pane_url local path must exist") }) } } func TestValidateConfig_WorkspaceNewPaneURLRejectsExistingBareRelativeFile(t *testing.T) { // This test mutates process CWD; do not add t.Parallel here. tmpDir := t.TempDir() oldWD, err := os.Getwd() require.NoError(t, err) require.NoError(t, os.Chdir(tmpDir)) t.Cleanup(func() { _ = os.Chdir(oldWD) }) require.NoError(t, os.WriteFile("README", []byte("ok"), 0644)) require.NoError(t, os.WriteFile("index.html", []byte("ok"), 0644)) for _, value := range []string{"README", "index.html"} { t.Run(value, func(t *testing.T) { cfg := DefaultConfig() cfg.Workspace.NewPaneURL = value validationErr := validateConfig(cfg) require.Error(t, validationErr) assert.Contains( t, validationErr.Error(), "workspace.new_pane_url relative local path cannot be resolved", ) }) } } func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingBareRelativeValue(t *testing.T) { // This test mutates process CWD; do not add t.Parallel here. tmpDir := t.TempDir() oldWD, err := os.Getwd() require.NoError(t, err) require.NoError(t, os.Chdir(tmpDir)) t.Cleanup(func() { _ = os.Chdir(oldWD) }) cfg := DefaultConfig() cfg.Workspace.NewPaneURL = "missing-local-file" validationErr := validateConfig(cfg) require.Error(t, validationErr) assert.Contains(t, validationErr.Error(), "workspace.new_pane_url") assert.Contains(t, validationErr.Error(), "relative local path cannot be resolved without config file context") } func TestValidateConfig_WebKitDefaultProfileIgnoresZeroGPUThreads(t *testing.T) { cfg := DefaultConfig() cfg.Engine.Profile = ProfileDefault cfg.Engine.WebKit.SkiaGPUPaintingThreads = 0 err := validateConfig(cfg) require.NoError(t, err) } func TestValidateConfig_OmniboxMaxHistoryDays(t *testing.T) { t.Run("default is valid", func(t *testing.T) { cfg := DefaultConfig() require.NoError(t, validateConfig(cfg)) }) t.Run("zero means all history and is valid", func(t *testing.T) { cfg := DefaultConfig() cfg.Omnibox.MaxHistoryDays = 0 require.NoError(t, validateConfig(cfg)) }) t.Run("negative is rejected", func(t *testing.T) { cfg := DefaultConfig() cfg.Omnibox.MaxHistoryDays = -1 err := validateConfig(cfg) require.Error(t, err) assert.Contains(t, err.Error(), "omnibox.max_history_days must be non-negative") }) }