package main import ( "os" "path/filepath" "strings" "testing" "time" ) func TestLoadConfigUsesDefaultsWhenOptionalFileIsMissing(t *testing.T) { path := filepath.Join(t.TempDir(), "missing.toml") got, err := loadConfig(path, false) if err != nil { t.Fatal(err) } want := defaultConfig() if got.Theme != want.Theme || got.RefreshInterval.Duration != want.RefreshInterval.Duration || got.Paths.Scroll != want.Paths.Scroll || got.Display.FoldResolved != want.Display.FoldResolved || got.Display.CompactReviews != want.Display.CompactReviews { t.Fatalf("defaults = %#v, want %#v", got, want) } } func TestLoadConfigParsesUserSettings(t *testing.T) { path := filepath.Join(t.TempDir(), "config.toml") content := ` theme = "light" refresh_interval = "25s" repository = "owner/repo" show_all = true limit = 75 endpoint = "https://github.example.com/api/graphql" [display] fold_resolved = false thread_list_width_percent = 45 dashboard_mode = "hotkey" compact_reviews = false [paths] scroll = true scroll_interval = "125ms" [threads] status_order = ["resolved", "unresolved", "outdated"] within_status = "timestamp" [cache] enabled = false max_age = "48h" directory = "/tmp/gh-threads-cache" ` if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatal(err) } got, err := loadConfig(path, true) if err != nil { t.Fatal(err) } if err := validateConfig(got); err != nil { t.Fatal(err) } if got.Theme != "light" || got.RefreshInterval.Duration != 25*time.Second || got.Repository != "owner/repo" || !got.ShowAll || got.Limit != 75 || got.Display.FoldResolved || got.Display.ThreadListWidthPercent != 45 || got.Display.DashboardMode != "hotkey" || got.Display.CompactReviews || !got.Paths.Scroll || got.Paths.ScrollInterval.Duration != 125*time.Millisecond || strings.Join(got.Threads.StatusOrder, ",") != "resolved,unresolved,outdated" || got.Threads.WithinStatus != "timestamp" || got.Cache.Enabled || got.Cache.MaxAge.Duration != 48*time.Hour || got.Cache.Directory != "/tmp/gh-threads-cache" { t.Fatalf("config = %#v", got) } } func TestLoadConfigRejectsUnknownSettings(t *testing.T) { path := filepath.Join(t.TempDir(), "config.toml") if err := os.WriteFile(path, []byte("refesh_interval = \"10s\"\n"), 0o600); err != nil { t.Fatal(err) } _, err := loadConfig(path, true) if err == nil || !strings.Contains(err.Error(), "refesh_interval") { t.Fatalf("unknown setting error = %v", err) } } func TestConfigPathHonorsEnvironmentOverride(t *testing.T) { t.Setenv("GH_THREADS_CONFIG", "/tmp/custom-gh-threads.toml") got, err := configPath() if err != nil { t.Fatal(err) } if got != "/tmp/custom-gh-threads.toml" { t.Fatalf("config path = %q", got) } } func TestExistingConfigPathFallsBackToDotConfig(t *testing.T) { root := t.TempDir() preferred := filepath.Join(root, "Library", "Application Support", "gh-threads", "config.toml") fallback := filepath.Join(root, ".config", "gh-threads", "config.toml") if err := os.MkdirAll(filepath.Dir(fallback), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(fallback, []byte("theme = \"dark\"\n"), 0o600); err != nil { t.Fatal(err) } if got := existingConfigPath(preferred, fallback); got != fallback { t.Fatalf("config path = %q, want fallback %q", got, fallback) } if err := os.MkdirAll(filepath.Dir(preferred), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(preferred, []byte("theme = \"light\"\n"), 0o600); err != nil { t.Fatal(err) } if got := existingConfigPath(preferred, fallback); got != preferred { t.Fatalf("config path = %q, want preferred %q", got, preferred) } } func TestConfigPathHonorsXDGConfigHome(t *testing.T) { t.Setenv("GH_THREADS_CONFIG", "") t.Setenv("XDG_CONFIG_HOME", "/tmp/xdg-config") got, err := configPath() if err != nil { t.Fatal(err) } want := "/tmp/xdg-config/gh-threads/config.toml" if got != want { t.Fatalf("config path = %q, want %q", got, want) } } func TestValidateConfigRejectsUnsafeAnimationRate(t *testing.T) { config := defaultConfig() config.Paths.ScrollInterval.Duration = 10 * time.Millisecond if err := validateConfig(config); err == nil { t.Fatal("unsafe path scrolling interval was accepted") } } func TestValidateConfigRejectsInvalidThreadOrdering(t *testing.T) { config := defaultConfig() config.Threads.StatusOrder = []string{"unresolved", "resolved", "resolved"} if err := validateConfig(config); err == nil { t.Fatal("duplicate thread status was accepted") } config = defaultConfig() config.Threads.WithinStatus = "author" if err := validateConfig(config); err == nil { t.Fatal("unknown within-status ordering was accepted") } } func TestValidateConfigRejectsInvalidDashboardMode(t *testing.T) { config := defaultConfig() config.Display.DashboardMode = "sometimes" if err := validateConfig(config); err == nil { t.Fatal("unknown dashboard mode was accepted") } }