package config import ( "os" "path/filepath" "strings" "testing" "time" "github.com/spf13/pflag" ) // clearEnv removes every GITFLOW_ variable so tests start from a known // state regardless of the developer's shell. func clearEnv(t *testing.T) { t.Helper() for _, kv := range os.Environ() { if strings.HasPrefix(kv, "GITFLOW_") { key := strings.SplitN(kv, "=", 2)[0] os.Unsetenv(key) } } } func loadWithFlags(t *testing.T, set func(f *pflag.FlagSet)) (*Config, error) { t.Helper() fs := NewFlagSet() if set != nil { set(fs) } return Load(fs) } func TestDefaults(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) // no config file present cfg, err := loadWithFlags(t, nil) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Dir != "." { t.Errorf("Dir = %q, want %q", cfg.Dir, ".") } if cfg.Interval != 0 || cfg.MaxDepth != 0 { t.Errorf("Interval/MaxDepth = %v/%d, want 0/0", cfg.Interval, cfg.MaxDepth) } if cfg.Format != "table" || cfg.Workers != 8 { t.Errorf("Format/Workers = %q/%d, want table/8", cfg.Format, cfg.Workers) } if cfg.AI.Provider != "openai" || cfg.AI.Model != "gpt-4o" || cfg.AI.APIKeyEnv != "OPENAI_API_KEY" { t.Errorf("AI defaults wrong: %+v", cfg.AI) } } func TestFlagOverrides(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) cfg, err := loadWithFlags(t, func(f *pflag.FlagSet) { if err := f.Set("format", "compact"); err != nil { t.Fatal(err) } if err := f.Set("dir", "/tmp/x"); err != nil { t.Fatal(err) } if err := f.Set("workers", "4"); err != nil { t.Fatal(err) } }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Format != "compact" || cfg.Dir != "/tmp/x" || cfg.Workers != 4 { t.Errorf("flag overrides not applied: %+v", cfg) } } func TestEnvOverrides(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) t.Setenv("GITFLOW_FORMAT", "json") t.Setenv("GITFLOW_MAX_DEPTH", "3") t.Setenv("GITFLOW_AI_PROVIDER", "ollama") cfg, err := loadWithFlags(t, nil) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Format != "json" || cfg.MaxDepth != 3 || cfg.AI.Provider != "ollama" { t.Errorf("env overrides not applied: %+v", cfg) } } func TestFlagBeatsEnv(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) t.Setenv("GITFLOW_FORMAT", "json") cfg, err := loadWithFlags(t, func(f *pflag.FlagSet) { if err := f.Set("format", "compact"); err != nil { t.Fatal(err) } }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Format != "compact" { t.Errorf("Format = %q, want compact (flag must beat env)", cfg.Format) } } func TestConfigFile(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) content := "dir: /home/user/projects\nformat: json\nmax_depth: 2\ninterval: 5m\nexclude:\n - node_modules\n - vendor\n" if err := os.WriteFile(filepath.Join(home, ".gitflow.yaml"), []byte(content), 0o644); err != nil { t.Fatal(err) } cfg, err := loadWithFlags(t, nil) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Dir != "/home/user/projects" || cfg.Format != "json" || cfg.MaxDepth != 2 { t.Errorf("file values not applied: %+v", cfg) } if cfg.Interval != 5*time.Minute { t.Errorf("Interval = %v, want 5m", cfg.Interval) } if len(cfg.Exclude) != 2 || cfg.Exclude[0] != "node_modules" || cfg.Exclude[1] != "vendor" { t.Errorf("Exclude = %v, want [node_modules vendor]", cfg.Exclude) } if cfg.ConfigFile != filepath.Join(home, ".gitflow.yaml") { t.Errorf("ConfigFile = %q, want %q", cfg.ConfigFile, filepath.Join(home, ".gitflow.yaml")) } } func TestConfigPathEnvOverride(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) alt := filepath.Join(home, "custom.yaml") if err := os.WriteFile(alt, []byte("format: compact\n"), 0o644); err != nil { t.Fatal(err) } t.Setenv("GITFLOW_CONFIG", alt) cfg, err := loadWithFlags(t, nil) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Format != "compact" { t.Errorf("Format = %q, want compact from GITFLOW_CONFIG file", cfg.Format) } } func TestValidateRejectsBadFormat(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) if _, err := loadWithFlags(t, func(f *pflag.FlagSet) { if err := f.Set("format", "xml"); err != nil { t.Fatal(err) } }); err == nil { t.Error("Load(bad format) succeeded, want error") } } func TestValidateRejectsBadWorkers(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) if _, err := loadWithFlags(t, func(f *pflag.FlagSet) { if err := f.Set("workers", "0"); err != nil { t.Fatal(err) } }); err == nil { t.Error("Load(workers=0) succeeded, want error") } } func TestDump(t *testing.T) { clearEnv(t) home := t.TempDir() t.Setenv("HOME", home) cfg, err := loadWithFlags(t, nil) if err != nil { t.Fatalf("Load: %v", err) } out, err := cfg.Dump() if err != nil { t.Fatalf("Dump: %v", err) } for _, want := range []string{"dir:", "interval:", "format:", "workers:", "ai:", "provider:"} { if !strings.Contains(out, want) { t.Errorf("Dump() missing %q:\n%s", want, out) } } }