package config import ( "os" "path/filepath" "runtime" "strings" "testing" ) func TestGlobalConfigDir_EnvPrecedence(t *testing.T) { // EECO_CONFIG_HOME wins outright. t.Setenv(GlobalConfigEnv, "/explicit/dir") t.Setenv("XDG_CONFIG_HOME", "/xdg") if got := GlobalConfigDir(); got != "/explicit/dir" { t.Fatalf("GlobalConfigDir() = %q, want /explicit/dir", got) } // With EECO_CONFIG_HOME empty, XDG_CONFIG_HOME/eeco is used. t.Setenv(GlobalConfigEnv, "") t.Setenv("XDG_CONFIG_HOME", "/xdg") if got, want := GlobalConfigDir(), filepath.Join("/xdg", "eeco"); got != want { t.Fatalf("GlobalConfigDir() = %q, want %q", got, want) } // With neither, fall back to /.config/eeco. os.UserHomeDir reads // $HOME on unix but %USERPROFILE% on Windows, so set the platform's var. t.Setenv(GlobalConfigEnv, "") t.Setenv("XDG_CONFIG_HOME", "") homeEnv := "HOME" if runtime.GOOS == "windows" { homeEnv = "USERPROFILE" } home := filepath.FromSlash("/home/tester") t.Setenv(homeEnv, home) if got, want := GlobalConfigDir(), filepath.Join(home, ".config", "eeco"); got != want { t.Fatalf("GlobalConfigDir() = %q, want %q", got, want) } } func TestLoad_ThreeLayerPrecedence(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") gdir := t.TempDir() t.Setenv(GlobalConfigEnv, gdir) write(t, gdir, LocalFilename, strings.Join([]string{ "automation=auto", "ai_budget=5", "stale_days=99", }, "\n")) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } // Local overrides one global key; the others fall through from global. write(t, wsDir, LocalFilename, strings.Join([]string{ "automation=manual", "context_path=ctx.md", }, "\n")) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.Automation != AutomationManual { t.Errorf("automation = %q, want manual (local wins)", cfg.Automation) } if cfg.AIBudget != 5 { t.Errorf("ai_budget = %d, want 5 (from global)", cfg.AIBudget) } if cfg.StaleDays != 99 { t.Errorf("stale_days = %d, want 99 (from global)", cfg.StaleDays) } if cfg.ContextPath != "ctx.md" { t.Errorf("context_path = %q, want ctx.md (local only)", cfg.ContextPath) } if cfg.BugReportDir != DefaultBugReportDir { t.Errorf("bug_report_dir = %q, want default %q", cfg.BugReportDir, DefaultBugReportDir) } } func TestLoad_GlobalAppliesWithoutWorkspace(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") gdir := t.TempDir() t.Setenv(GlobalConfigEnv, gdir) write(t, gdir, LocalFilename, "automation=scaffold\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.Automation != AutomationScaffold { t.Errorf("automation = %q, want scaffold (global applies pre-init)", cfg.Automation) } } func TestKnownKeysAndEffectiveValue(t *testing.T) { if !KnownKey("automation") || KnownKey("definitely_not_a_key") { t.Fatal("KnownKey mis-classified a key") } if len(KnownKeys()) < 20 { t.Fatalf("KnownKeys() returned %d keys, expected the full set", len(KnownKeys())) } root := newRepo(t) write(t, root, "go.mod", "module x\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if v, ok := EffectiveValue(cfg, "automation"); !ok || v != string(DefaultAutomation) { t.Errorf("EffectiveValue(automation) = %q,%v, want %q,true", v, ok, DefaultAutomation) } if _, ok := EffectiveValue(cfg, "nope"); ok { t.Error("EffectiveValue returned ok for an unknown key") } } func TestValidateSetValue(t *testing.T) { cases := []struct { key, val string wantErr bool }{ {"automation", "auto", false}, {"ai_budget", "3", false}, {"context_path", "x.md", false}, // Floor-invariant enum keys tolerate any value (normalized at load), // so set mirrors load and does NOT reject them. {"automation", "bogus", false}, // Strictly-typed keys reject malformed values at parse time. {"ai_budget", "notanint", true}, {"stale_days", "notanint", true}, {"init_detection_threshold", "2", true}, // out of [0,1] {"no_such_key", "x", true}, // unknown key } for _, c := range cases { err := ValidateSetValue(c.key, c.val) if (err != nil) != c.wantErr { t.Errorf("ValidateSetValue(%q,%q) err=%v, wantErr=%v", c.key, c.val, err, c.wantErr) } } } func TestWriteGlobalKeys_UpsertsAndCreatesDir(t *testing.T) { gdir := filepath.Join(t.TempDir(), "nested", "eeco") // does not exist yet t.Setenv(GlobalConfigEnv, gdir) if err := WriteGlobalKeys(map[string]string{"automation": "auto"}); err != nil { t.Fatal(err) } if err := WriteGlobalKeys(map[string]string{"ai_budget": "3"}); err != nil { t.Fatal(err) } // Overwrite an existing key in place. if err := WriteGlobalKeys(map[string]string{"automation": "manual"}); err != nil { t.Fatal(err) } b, err := os.ReadFile(filepath.Join(gdir, LocalFilename)) if err != nil { t.Fatal(err) } got := string(b) if !strings.Contains(got, "automation=manual") { t.Errorf("global file missing automation=manual:\n%s", got) } if !strings.Contains(got, "ai_budget=3") { t.Errorf("global file missing ai_budget=3:\n%s", got) } if strings.Contains(got, "automation=auto") { t.Errorf("global file kept stale automation=auto:\n%s", got) } // And the value resolves through Load. root := newRepo(t) write(t, root, "go.mod", "module x\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.Automation != AutomationManual { t.Errorf("automation = %q, want manual after WriteGlobalKeys", cfg.Automation) } } func TestParseLocalFile(t *testing.T) { if m, err := ParseLocalFile(""); err != nil || len(m) != 0 { t.Fatalf("ParseLocalFile(empty) = %v,%v want empty,nil", m, err) } dir := t.TempDir() p := filepath.Join(dir, LocalFilename) content := "# comment\n\nautomation=auto\nai_budget = \"3\"\ngate=go build\ngate=go vet\nattribution_pattern=a=b\nbroken_line_without_eq\n" if err := os.WriteFile(p, []byte(content), 0o644); err != nil { t.Fatal(err) } m, err := ParseLocalFile(p) if err != nil { t.Fatal(err) } if m["automation"] != "auto" { t.Errorf("automation = %q", m["automation"]) } if m["ai_budget"] != "3" { t.Errorf("ai_budget = %q, want 3 (quotes stripped)", m["ai_budget"]) } if m["gate"] != "go vet" { t.Errorf("gate = %q, want last-wins go vet", m["gate"]) } if m["attribution_pattern"] != "a=b" { t.Errorf("attribution_pattern = %q, want a=b (split on first =)", m["attribution_pattern"]) } if _, ok := m["broken_line_without_eq"]; ok { t.Error("a line without '=' should be skipped") } if m2, err := ParseLocalFile(filepath.Join(dir, "missing")); err != nil || len(m2) != 0 { t.Fatalf("ParseLocalFile(missing) = %v,%v want empty,nil", m2, err) } // TestMain pins EECO_CONFIG_HOME, so the global path resolves non-empty. if GlobalConfigLocalPath() == "" { t.Error("GlobalConfigLocalPath() empty despite EECO_CONFIG_HOME set") } } func TestDeclaredKeys(t *testing.T) { if keys, err := DeclaredKeys(""); err != nil || len(keys) != 0 { t.Fatalf("DeclaredKeys(empty) = %v,%v, want empty,nil", keys, err) } dir := t.TempDir() path := filepath.Join(dir, LocalFilename) if err := os.WriteFile(path, []byte("# c\n\nautomation=auto\nai_budget=2\nunknown_key=x\n"), 0o644); err != nil { t.Fatal(err) } keys, err := DeclaredKeys(path) if err != nil { t.Fatal(err) } for _, want := range []string{"automation", "ai_budget", "unknown_key"} { if !keys[want] { t.Errorf("DeclaredKeys missing %q", want) } } if keys["nonexistent"] { t.Error("DeclaredKeys reported a key not in the file") } }