package config import ( "os" "path/filepath" "reflect" "strings" "testing" ) // TestMain pins the workspace owner so Load resolves a deterministic // username across machines instead of picking up the dev box's // `git config user.name`. Every Load in this package then scopes the // workspace under /tester/.eeco. func TestMain(m *testing.M) { os.Setenv("EECO_USERNAME", "tester") // Pin the user-global config dir to an empty temp dir so the global // layer is a hermetic no-op and tests never read the dev box's // ~/.config/eeco. Tests that exercise the global layer override via // t.Setenv(GlobalConfigEnv, ...). gdir, err := os.MkdirTemp("", "eeco-global-") if err != nil { panic(err) } os.Setenv(GlobalConfigEnv, gdir) code := m.Run() os.RemoveAll(gdir) os.Exit(code) } func TestFindRepoRoot_WalksUpToDotGit(t *testing.T) { root := t.TempDir() if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil { t.Fatal(err) } deep := filepath.Join(root, "a", "b", "c") if err := os.MkdirAll(deep, 0o755); err != nil { t.Fatal(err) } got, err := FindRepoRoot(deep) if err != nil { t.Fatalf("FindRepoRoot(%q) error: %v", deep, err) } wantRoot, _ := filepath.EvalSymlinks(root) gotRoot, _ := filepath.EvalSymlinks(got) if gotRoot != wantRoot { t.Fatalf("FindRepoRoot = %q, want %q", gotRoot, wantRoot) } } func TestFindRepoRoot_AcceptsGitFile(t *testing.T) { // Worktrees use a `.git` *file* with a gitdir pointer; FindRepoRoot // must accept that too. root := t.TempDir() if err := os.WriteFile(filepath.Join(root, ".git"), []byte("gitdir: x\n"), 0o644); err != nil { t.Fatal(err) } got, err := FindRepoRoot(root) if err != nil { t.Fatal(err) } wantRoot, _ := filepath.EvalSymlinks(root) gotRoot, _ := filepath.EvalSymlinks(got) if gotRoot != wantRoot { t.Fatalf("FindRepoRoot = %q, want %q", gotRoot, wantRoot) } } func TestFindRepoRoot_ErrorsOutsideRepo(t *testing.T) { // A fresh temp directory should not be inside any git repo on any // sane test machine; if it is, the test environment is broken. dir := t.TempDir() if _, err := FindRepoRoot(dir); err == nil { t.Fatal("expected error outside repo, got nil") } } // newHostWithPrivateRepo builds a host repo (/.git) containing eeco's // private workspace-history repo at /tester/.git with the engine // workspace /tester/.eeco beside it — the on-disk shape `eeco init` // leaves and the cwd the harness launches from to load the emitted cockpit. // It returns the host root and the private workspace dir (/tester). func newHostWithPrivateRepo(t *testing.T) (host, priv string) { t.Helper() host = newRepo(t) priv = filepath.Join(host, "tester") if err := os.MkdirAll(filepath.Join(priv, ".git"), 0o755); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(priv, DefaultWorkspace), 0o755); err != nil { t.Fatal(err) } return host, priv } func TestResolveProjectRoot_SkipsPrivateWorkspaceRepo(t *testing.T) { // FIX-1: from inside / (and deeper), root detection must walk // past the private /.git and resolve the host project root. host, priv := newHostWithPrivateRepo(t) wantRoot, _ := filepath.EvalSymlinks(host) for _, start := range []string{priv, filepath.Join(priv, DefaultWorkspace, "memory")} { if err := os.MkdirAll(start, 0o755); err != nil { t.Fatal(err) } got, err := resolveProjectRoot(start) if err != nil { t.Fatalf("resolveProjectRoot(%q) error: %v", start, err) } gotRoot, _ := filepath.EvalSymlinks(got) if gotRoot != wantRoot { t.Fatalf("resolveProjectRoot(%q) = %q, want host root %q (must skip the private /.git)", start, gotRoot, wantRoot) } } } func TestResolveProjectRoot_NormalRepoUnchanged(t *testing.T) { // A repo with no .eeco beside its .git resolves exactly like FindRepoRoot. root := newRepo(t) deep := filepath.Join(root, "a", "b") if err := os.MkdirAll(deep, 0o755); err != nil { t.Fatal(err) } got, err := resolveProjectRoot(deep) if err != nil { t.Fatal(err) } wantRoot, _ := filepath.EvalSymlinks(root) gotRoot, _ := filepath.EvalSymlinks(got) if gotRoot != wantRoot { t.Fatalf("resolveProjectRoot = %q, want %q", gotRoot, wantRoot) } } func TestResolveProjectRoot_PrivateOnlyFallsBack(t *testing.T) { // A private workspace repo with no host repo above it (a shape eeco init // never produces) falls back to the private repo rather than erroring, so // the fix is never worse than a plain FindRepoRoot. base := t.TempDir() priv := filepath.Join(base, "tester") if err := os.MkdirAll(filepath.Join(priv, ".git"), 0o755); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(priv, DefaultWorkspace), 0o755); err != nil { t.Fatal(err) } got, err := resolveProjectRoot(priv) if err != nil { t.Fatalf("resolveProjectRoot fallback error: %v", err) } wantRoot, _ := filepath.EvalSymlinks(priv) gotRoot, _ := filepath.EvalSymlinks(got) if gotRoot != wantRoot { t.Fatalf("resolveProjectRoot fallback = %q, want private repo %q", gotRoot, wantRoot) } } func TestLoad_FromInsidePrivateWorkspaceRepo(t *testing.T) { // The FIX-1 repro end-to-end: launched from //, Load must // resolve the host repo root and the real workspace, not the nested // private repo (which made ///.eeco missing). host, priv := newHostWithPrivateRepo(t) write(t, host, "go.mod", "module x\n") cfg, err := Load(priv, "") if err != nil { t.Fatal(err) } wantRoot, _ := filepath.EvalSymlinks(host) if gotRoot, _ := filepath.EvalSymlinks(cfg.RepoRoot); gotRoot != wantRoot { t.Fatalf("RepoRoot = %q, want host root %q", gotRoot, wantRoot) } wantUserDir, _ := filepath.EvalSymlinks(priv) if gotUserDir, _ := filepath.EvalSymlinks(cfg.UserDir); gotUserDir != wantUserDir { t.Fatalf("UserDir = %q, want %q", gotUserDir, wantUserDir) } wantWS, _ := filepath.EvalSymlinks(filepath.Join(host, "tester", DefaultWorkspace)) if gotWS, _ := filepath.EvalSymlinks(cfg.Workspace); gotWS != wantWS { t.Fatalf("Workspace = %q, want %q", gotWS, wantWS) } } func TestDetectProfile(t *testing.T) { cases := []struct { name string seed map[string]string // path -> contents want Profile }{ {"go", map[string]string{"go.mod": "module x\n"}, ProfileGo}, {"zig", map[string]string{"build.zig": ""}, ProfileZig}, {"rust", map[string]string{"Cargo.toml": "[package]\n"}, ProfileRust}, {"node", map[string]string{"package.json": "{}"}, ProfileNode}, {"python-pyproject", map[string]string{"pyproject.toml": ""}, ProfilePython}, {"python-requirements", map[string]string{"requirements.txt": ""}, ProfilePython}, {"python-requirements-dev", map[string]string{"requirements-dev.txt": ""}, ProfilePython}, {"python-venv", map[string]string{".venv/bin/python": ""}, ProfilePython}, {"generic-empty", map[string]string{}, ProfileGeneric}, {"generic-random", map[string]string{"some-file.txt": "x"}, ProfileGeneric}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() for path, content := range tc.seed { full := filepath.Join(dir, path) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(content), 0o644); err != nil { t.Fatal(err) } } got := DetectProfile(dir) if got != tc.want { t.Fatalf("DetectProfile = %q, want %q", got, tc.want) } }) } } func TestDetectProfile_GoWinsOverPython(t *testing.T) { // A polyglot repo with both go.mod and pyproject.toml resolves to // the documented precedence order (Go first). dir := t.TempDir() write(t, dir, "go.mod", "module x\n") write(t, dir, "pyproject.toml", "") if got := DetectProfile(dir); got != ProfileGo { t.Fatalf("DetectProfile polyglot = %q, want %q", got, ProfileGo) } } func TestGateFor(t *testing.T) { cases := map[Profile][][]string{ ProfileGo: {{"go", "vet", "./..."}}, ProfileZig: {{"zig", "build", "--summary", "none"}}, ProfileRust: {{"cargo", "check", "--quiet"}}, ProfileNode: {{"npm", "run", "--if-present", "typecheck"}}, ProfilePython: {{"python3", "-m", "compileall", "-q", "."}}, ProfileGeneric: nil, } for p, want := range cases { got := GateFor(p) if !reflect.DeepEqual(got, want) { t.Errorf("GateFor(%q) = %v, want %v", p, got, want) } } } func TestGateFor_ReturnsFreshSlice(t *testing.T) { a := GateFor(ProfileGo) b := GateFor(ProfileGo) a[0][0] = "MUTATED" if b[0][0] == "MUTATED" { t.Fatal("GateFor returned a shared backing array; expected a fresh copy") } } func TestLoad_DefaultsAndRepoRoot(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if got, want := cfg.WorkspaceName, DefaultWorkspace; got != want { t.Errorf("workspace name = %q, want %q", got, want) } wantWS := filepath.Join(root, "tester", DefaultWorkspace) gotWS, _ := filepath.EvalSymlinks(filepath.Dir(cfg.Workspace)) wantWSDir, _ := filepath.EvalSymlinks(filepath.Dir(wantWS)) if gotWS != wantWSDir { t.Errorf("workspace parent = %q, want %q", gotWS, wantWSDir) } if cfg.Profile != ProfileGo { t.Errorf("profile = %q, want %q", cfg.Profile, ProfileGo) } if !reflect.DeepEqual(cfg.Gate, [][]string{{"go", "vet", "./..."}}) { t.Errorf("gate = %v", cfg.Gate) } } func TestLoad_RejectsBadWorkspaceName(t *testing.T) { root := newRepo(t) cases := []string{"..", ".", "foo/bar", "/abs", `back\slash`} for _, name := range cases { if _, err := Load(root, name); err == nil { t.Errorf("Load(%q) succeeded; expected error", name) } } } func TestLoad_ConfigLocalOverride(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ "# comment line", "", `profile = "generic"`, "gate=make check", "unknown_key=ignored", }, "\n")) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.Profile != ProfileGeneric { t.Errorf("profile override = %q, want generic", cfg.Profile) } if !reflect.DeepEqual(cfg.Gate, [][]string{{"make", "check"}}) { t.Errorf("gate override = %v, want [[make check]]", cfg.Gate) } } func TestLoad_ConfigLocalProfileResetsGate(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "profile=rust\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.Profile != ProfileRust { t.Fatalf("profile = %q, want rust", cfg.Profile) } if !reflect.DeepEqual(cfg.Gate, [][]string{{"cargo", "check", "--quiet"}}) { t.Fatalf("gate after profile override = %v", cfg.Gate) } } func TestLoad_ConfigLocalEmptyGateDisablesIt(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "gate=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.Gate != nil { t.Fatalf("gate = %v, want nil", cfg.Gate) } } func TestLoad_ConfigLocalMultiStepGate(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } // Three `gate=` lines: the first resets the profile default, all // three append, so the chain runs in declared order. write(t, wsDir, "config.local", strings.Join([]string{ "gate=go vet ./...", "gate=staticcheck ./...", "gate=go test ./...", }, "\n")) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := [][]string{ {"go", "vet", "./..."}, {"staticcheck", "./..."}, {"go", "test", "./..."}, } if !reflect.DeepEqual(cfg.Gate, want) { t.Fatalf("gate chain = %v, want %v", cfg.Gate, want) } } func TestLoad_DefaultStaleDays(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.StaleDays != DefaultStaleDays { t.Errorf("StaleDays = %d, want %d", cfg.StaleDays, DefaultStaleDays) } } func TestLoad_ConfigLocalStaleDaysOverride(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "stale_days=7\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.StaleDays != 7 { t.Errorf("StaleDays = %d, want 7", cfg.StaleDays) } } func TestLoad_ConfigLocalStaleDaysMalformed(t *testing.T) { cases := []string{"stale_days=abc\n", "stale_days=-3\n"} for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_ConfigLocalInitDetectionThreshold(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "init_detection_threshold=0.85\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.InitDetectionThreshold != 0.85 { t.Errorf("InitDetectionThreshold = %v, want 0.85", cfg.InitDetectionThreshold) } } func TestLoad_ConfigLocalInitDetectionThresholdMalformed(t *testing.T) { cases := []string{ "init_detection_threshold=abc\n", "init_detection_threshold=2\n", "init_detection_threshold=-0.1\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_ConfigLocalMalformed(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "no-equals-sign-here\n") if _, err := Load(root, ""); err == nil { t.Fatal("expected malformed config.local to error") } } func TestLoad_SessionSettingsPath(t *testing.T) { t.Run("unset default is empty", func(t *testing.T) { t.Setenv("EECO_SESSION_SETTINGS", "") root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.SessionSettingsPath != "" { t.Errorf("SessionSettingsPath = %q, want empty", cfg.SessionSettingsPath) } }) t.Run("env supplies the default", func(t *testing.T) { t.Setenv("EECO_SESSION_SETTINGS", "/abs/env/settings.json") root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.SessionSettingsPath != "/abs/env/settings.json" { t.Errorf("SessionSettingsPath = %q, want the env value", cfg.SessionSettingsPath) } }) t.Run("config.local overrides env", func(t *testing.T) { envPath := filepath.Join(t.TempDir(), "env-settings.json") localPath := filepath.Join(t.TempDir(), "local-settings.json") t.Setenv("EECO_SESSION_SETTINGS", envPath) root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "session_settings_path="+localPath+"\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.SessionSettingsPath != localPath { t.Errorf("SessionSettingsPath = %q, want the config.local value", cfg.SessionSettingsPath) } }) t.Run("empty value clears the env default", func(t *testing.T) { t.Setenv("EECO_SESSION_SETTINGS", "/abs/env/settings.json") root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "session_settings_path=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.SessionSettingsPath != "" { t.Errorf("SessionSettingsPath = %q, want empty after explicit clear", cfg.SessionSettingsPath) } }) t.Run("relative path is rejected", func(t *testing.T) { t.Setenv("EECO_SESSION_SETTINGS", "") root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "session_settings_path=rel/settings.json\n") if _, err := Load(root, ""); err == nil { t.Fatal("expected a relative session_settings_path to error") } }) } func TestLoad_DefaultBugReportDir(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.BugReportDir != DefaultBugReportDir { t.Errorf("BugReportDir = %q, want %q", cfg.BugReportDir, DefaultBugReportDir) } } func TestLoad_ConfigLocalBugReportDirOverride(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "bug_report_dir=my-bugs\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.BugReportDir != "my-bugs" { t.Errorf("BugReportDir = %q, want my-bugs", cfg.BugReportDir) } } func TestLoad_ConfigLocalBugReportDirEmptyResetsToDefault(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "bug_report_dir=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.BugReportDir != DefaultBugReportDir { t.Errorf("BugReportDir = %q, want default %q", cfg.BugReportDir, DefaultBugReportDir) } } func TestLoad_ConfigLocalBugReportDirRejected(t *testing.T) { cases := []string{ "bug_report_dir=/abs/path\n", "bug_report_dir=..\n", "bug_report_dir=../escape\n", "bug_report_dir=sub/../../escape\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_DefaultContextPath(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.ContextPath != DefaultContextPath { t.Errorf("ContextPath = %q, want %q", cfg.ContextPath, DefaultContextPath) } } func TestLoad_ConfigLocalContextPathOverride(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "context_path=brief/project.md\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.ContextPath != "brief/project.md" { t.Errorf("ContextPath = %q, want brief/project.md", cfg.ContextPath) } } func TestLoad_ConfigLocalContextPathEmptyResetsToDefault(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "context_path=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.ContextPath != DefaultContextPath { t.Errorf("ContextPath = %q, want default %q", cfg.ContextPath, DefaultContextPath) } } func TestLoad_ConfigLocalContextPathRejected(t *testing.T) { cases := []string{ "context_path=/abs/path.md\n", "context_path=..\n", "context_path=../escape.md\n", "context_path=sub/../../escape.md\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_DefaultContextBudget(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.ContextBudget != DefaultContextBudget { t.Errorf("ContextBudget = %d, want %d", cfg.ContextBudget, DefaultContextBudget) } } func TestLoad_ConfigLocalContextBudgetOverride(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "context_budget=800\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.ContextBudget != 800 { t.Errorf("ContextBudget = %d, want 800", cfg.ContextBudget) } } func TestLoad_ConfigLocalContextBudgetEmptyResetsToDefault(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "context_budget=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.ContextBudget != DefaultContextBudget { t.Errorf("ContextBudget = %d, want default %d", cfg.ContextBudget, DefaultContextBudget) } } func TestLoad_ConfigLocalContextBudgetRejected(t *testing.T) { cases := []string{ "context_budget=-1\n", "context_budget=notanumber\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_DefaultBriefIncludeNotes(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.BriefIncludeNotes != DefaultBriefIncludeNotes { t.Errorf("BriefIncludeNotes = %v, want %v", cfg.BriefIncludeNotes, DefaultBriefIncludeNotes) } } func TestLoad_ConfigLocalBriefIncludeNotesAccepted(t *testing.T) { cases := []struct { body string want bool }{ {"brief_include_notes=true\n", true}, {"brief_include_notes=false\n", false}, {"brief_include_notes=1\n", true}, {"brief_include_notes=0\n", false}, {"brief_include_notes=\n", DefaultBriefIncludeNotes}, } for _, tc := range cases { t.Run(tc.body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.BriefIncludeNotes != tc.want { t.Errorf("BriefIncludeNotes = %v, want %v", cfg.BriefIncludeNotes, tc.want) } }) } } func TestLoad_ConfigLocalBriefIncludeNotesRejected(t *testing.T) { cases := []string{ "brief_include_notes=yes\n", "brief_include_notes=no\n", "brief_include_notes=notabool\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_DefaultSessionStartPinnedBodies(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.SessionStartPinnedBodies != DefaultSessionStartPinnedBodies { t.Errorf("SessionStartPinnedBodies = %v, want %v", cfg.SessionStartPinnedBodies, DefaultSessionStartPinnedBodies) } } func TestLoad_ConfigLocalSessionStartPinnedBodiesAccepted(t *testing.T) { cases := []struct { body string want bool }{ {"session_start_pinned_bodies=true\n", true}, {"session_start_pinned_bodies=false\n", false}, {"session_start_pinned_bodies=1\n", true}, {"session_start_pinned_bodies=0\n", false}, {"session_start_pinned_bodies=\n", DefaultSessionStartPinnedBodies}, } for _, tc := range cases { t.Run(tc.body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.SessionStartPinnedBodies != tc.want { t.Errorf("SessionStartPinnedBodies = %v, want %v", cfg.SessionStartPinnedBodies, tc.want) } }) } } func TestLoad_ConfigLocalSessionStartPinnedBodiesRejected(t *testing.T) { cases := []string{ "session_start_pinned_bodies=yes\n", "session_start_pinned_bodies=no\n", "session_start_pinned_bodies=notabool\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_DefaultVersionLocationsEmpty(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if len(cfg.VersionLocations) != 0 { t.Errorf("VersionLocations = %v, want empty", cfg.VersionLocations) } } func TestLoad_ConfigLocalVersionLocationsRepeatable(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ `version_locations=CHANGELOG.md:^## \[v(\d+\.\d+\.\d+)\]`, `version_locations=VERSION:^v(\d+\.\d+\.\d+)`, "", "version_locations=", // blank — ignored, no phantom entry }, "\n")+"\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{ `CHANGELOG.md:^## \[v(\d+\.\d+\.\d+)\]`, `VERSION:^v(\d+\.\d+\.\d+)`, } if !reflect.DeepEqual(cfg.VersionLocations, want) { t.Fatalf("VersionLocations = %v, want %v", cfg.VersionLocations, want) } } func TestLoad_ConfigLocalVersionLocationsRejected(t *testing.T) { cases := []string{ "version_locations=no-colon-here\n", "version_locations=:no-path\n", "version_locations=/abs/path:v(\\d+)\n", "version_locations=..:v(\\d+)\n", "version_locations=../escape.md:v(\\d+)\n", "version_locations=sub/../../escape.md:v(\\d+)\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_ConfigLocalVersionLocationsAuto(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "version_locations=auto\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"auto"} if !reflect.DeepEqual(cfg.VersionLocations, want) { t.Fatalf("VersionLocations = %v, want %v", cfg.VersionLocations, want) } } func TestLoad_ConfigLocalVersionLocationsAutoRejectsMix(t *testing.T) { cases := map[string]string{ "auto then explicit": "version_locations=auto\n" + `version_locations=VERSION:v(\d+\.\d+\.\d+)` + "\n", "explicit then auto": `version_locations=VERSION:v(\d+\.\d+\.\d+)` + "\n" + "version_locations=auto\n", "auto twice": "version_locations=auto\nversion_locations=auto\n", } for name, body := range cases { t.Run(name, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_DefaultVersionAnchorEmpty(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.VersionAnchor != "" { t.Errorf("VersionAnchor default = %q, want empty", cfg.VersionAnchor) } } func TestLoad_ConfigLocalVersionAnchorTag(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "version_anchor=tag\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.VersionAnchor != "tag" { t.Errorf("VersionAnchor = %q, want %q", cfg.VersionAnchor, "tag") } } func TestLoad_ConfigLocalVersionAnchorFile(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } body := `version_anchor=VERSION:^v(\d+\.\d+\.\d+)` + "\n" write(t, wsDir, "config.local", body) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := `VERSION:^v(\d+\.\d+\.\d+)` if cfg.VersionAnchor != want { t.Errorf("VersionAnchor = %q, want %q", cfg.VersionAnchor, want) } } func TestLoad_ConfigLocalVersionAnchorEmptyResetsToDefault(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "version_anchor=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.VersionAnchor != "" { t.Errorf("VersionAnchor = %q, want empty", cfg.VersionAnchor) } } func TestLoad_ConfigLocalVersionAnchorRejected(t *testing.T) { cases := []string{ "version_anchor=no-colon-here\n", "version_anchor=:no-path\n", "version_anchor=VERSION:\n", "version_anchor=/abs/path:v(\\d+)\n", "version_anchor=..:v(\\d+)\n", "version_anchor=../escape.md:v(\\d+)\n", "version_anchor=sub/../../escape.md:v(\\d+)\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestLoad_DefaultPreCommitWorkflows(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"leak-guard", "version-sync"} if !reflect.DeepEqual(cfg.PreCommitWorkflows, want) { t.Errorf("PreCommitWorkflows default = %v, want %v", cfg.PreCommitWorkflows, want) } } func TestLoad_ConfigLocalPreCommitWorkflowsReplacesDefault(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ "pre_commit_workflows=leak-guard", "pre_commit_workflows=comment-hygiene", "", "pre_commit_workflows=", // blank — ignored, no phantom entry }, "\n")+"\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"leak-guard", "comment-hygiene"} if !reflect.DeepEqual(cfg.PreCommitWorkflows, want) { t.Fatalf("PreCommitWorkflows = %v, want %v", cfg.PreCommitWorkflows, want) } } func TestLoad_ConfigLocalPreCommitWorkflowsEmptyDisables(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "pre_commit_workflows=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if len(cfg.PreCommitWorkflows) != 0 { t.Errorf("PreCommitWorkflows = %v, want empty (default disabled)", cfg.PreCommitWorkflows) } } func TestLoad_ConfigLocalPreCommitWorkflowsRejectsWhitespace(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "pre_commit_workflows=leak-guard version-sync\n") if _, err := Load(root, ""); err == nil { t.Fatal("expected error on whitespace-containing workflow name") } } func TestLoad_DefaultPostMergeWorkflows(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"memory-drift", "doc-drift", "manifest-refresh", "cockpit-sync"} if !reflect.DeepEqual(cfg.PostMergeWorkflows, want) { t.Errorf("PostMergeWorkflows default = %v, want %v", cfg.PostMergeWorkflows, want) } } func TestLoad_ConfigLocalPostMergeWorkflowsReplacesDefault(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ "post_merge_workflows=memory-drift", "post_merge_workflows=", // blank — ignored, no phantom entry }, "\n")+"\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"memory-drift"} if !reflect.DeepEqual(cfg.PostMergeWorkflows, want) { t.Fatalf("PostMergeWorkflows = %v, want %v", cfg.PostMergeWorkflows, want) } } func TestLoad_ConfigLocalPostMergeWorkflowsEmptyDisables(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "post_merge_workflows=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if len(cfg.PostMergeWorkflows) != 0 { t.Errorf("PostMergeWorkflows = %v, want empty (default disabled)", cfg.PostMergeWorkflows) } } func TestLoad_ConfigLocalPostMergeWorkflowsRejectsWhitespace(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "post_merge_workflows=memory-drift doc-drift\n") if _, err := Load(root, ""); err == nil { t.Fatal("expected error on whitespace-containing workflow name") } } func TestLoad_DefaultSessionFiles(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if len(cfg.SessionFiles) != 0 { t.Errorf("SessionFiles default = %v, want empty", cfg.SessionFiles) } } func TestLoad_ConfigLocalSessionFilesRepoRelative(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ "session_files=CLAUDE.md", "session_files=AGENTS.md", "", }, "\n")) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"CLAUDE.md", "AGENTS.md"} if !reflect.DeepEqual(cfg.SessionFiles, want) { t.Errorf("SessionFiles = %v, want %v", cfg.SessionFiles, want) } } func TestLoad_ConfigLocalSessionFilesAbsoluteAccepted(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } abs := filepath.Join(t.TempDir(), "cursor-rules.md") write(t, wsDir, "config.local", "session_files="+abs+"\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if len(cfg.SessionFiles) != 1 || cfg.SessionFiles[0] != abs { t.Errorf("SessionFiles = %v, want [%q]", cfg.SessionFiles, abs) } } func TestLoad_ConfigLocalSessionFilesMixed(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } abs := filepath.Join(t.TempDir(), "cursor-rules.md") write(t, wsDir, "config.local", strings.Join([]string{ "session_files=CLAUDE.md", "session_files=" + abs, "", }, "\n")) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"CLAUDE.md", abs} if !reflect.DeepEqual(cfg.SessionFiles, want) { t.Errorf("SessionFiles = %v, want %v", cfg.SessionFiles, want) } } func TestLoad_ConfigLocalSessionFilesRejected(t *testing.T) { cases := []string{ "session_files=..\n", "session_files=../escape.md\n", "session_files=sub/../../escape.md\n", "session_files=has space.md\n", } for _, body := range cases { t.Run(body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } func TestWriteLocalKeys_UpsertPreservesAndRoundTrips(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ "# keep me", "stale_days=7", "unknown_key=keep", }, "\n")+"\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if err := WriteLocalKeys(cfg, map[string]string{ "stale_days": "9", // replace in place "automation": "scaffold", // append "ai_budget": "3", // append }); err != nil { t.Fatal(err) } b, _ := os.ReadFile(filepath.Join(wsDir, "config.local")) got := string(b) if !strings.Contains(got, "# keep me") || !strings.Contains(got, "unknown_key=keep") { t.Errorf("comments / unknown keys not preserved:\n%s", got) } if strings.Contains(got, "stale_days=7") || !strings.Contains(got, "stale_days=9") { t.Errorf("stale_days not replaced in place:\n%s", got) } // The override must round-trip through Load. cfg2, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg2.StaleDays != 9 { t.Errorf("StaleDays = %d, want 9", cfg2.StaleDays) } if cfg2.Automation != AutomationScaffold { t.Errorf("Automation = %q, want scaffold", cfg2.Automation) } if cfg2.AIBudget != 3 { t.Errorf("AIBudget = %d, want 3", cfg2.AIBudget) } } func TestWriteLocalKeys_CreatesFileWhenMissing(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if err := WriteLocalKeys(cfg, map[string]string{"automation": "auto"}); err != nil { t.Fatal(err) } cfg2, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg2.Automation != AutomationAuto { t.Errorf("Automation = %q, want auto", cfg2.Automation) } } func TestWriteLocalKeys_RequiresInitialisedWorkspace(t *testing.T) { root := newRepo(t) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if err := WriteLocalKeys(cfg, map[string]string{"automation": "auto"}); err == nil { t.Fatal("expected an error when the workspace is not initialised") } } // --- helpers --- func newRepo(t *testing.T) string { t.Helper() root := t.TempDir() if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil { t.Fatal(err) } return root } func write(t *testing.T, dir, name, content string) { t.Helper() full := filepath.Join(dir, name) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(content), 0o644); err != nil { t.Fatal(err) } } func TestLoad_DefaultWorkspaceHistory(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.WorkspaceHistory != DefaultWorkspaceHistory { t.Errorf("WorkspaceHistory = %q, want %q (default)", cfg.WorkspaceHistory, DefaultWorkspaceHistory) } if DefaultWorkspaceHistory != WorkspaceHistoryManual { t.Errorf("DefaultWorkspaceHistory = %q, want manual (safe-default floor)", DefaultWorkspaceHistory) } } func TestLoad_ConfigLocalWorkspaceHistory(t *testing.T) { cases := []struct { val string want WorkspaceHistory }{ {"off", WorkspaceHistoryOff}, {"manual", WorkspaceHistoryManual}, {"auto", WorkspaceHistoryAuto}, {"", WorkspaceHistoryManual}, // empty resets to default {"nonsense", WorkspaceHistoryManual}, // unknown → default (floor) } for _, tc := range cases { t.Run(tc.val, func(t *testing.T) { root := newRepo(t) write(t, root, "go.mod", "module x\n") wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "workspace_history="+tc.val+"\n") cfg, err := Load(root, "") if err != nil { t.Fatalf("Load: %v", err) } if cfg.WorkspaceHistory != tc.want { t.Errorf("workspace_history=%q → %q, want %q", tc.val, cfg.WorkspaceHistory, tc.want) } }) } } func TestWorkspaceHistory_EnabledAuto(t *testing.T) { cases := []struct { h WorkspaceHistory enabled bool auto bool }{ {WorkspaceHistoryOff, false, false}, {WorkspaceHistoryManual, true, false}, {WorkspaceHistoryAuto, true, true}, } for _, tc := range cases { if got := tc.h.Enabled(); got != tc.enabled { t.Errorf("%q.Enabled() = %v, want %v", tc.h, got, tc.enabled) } if got := tc.h.Auto(); got != tc.auto { t.Errorf("%q.Auto() = %v, want %v", tc.h, got, tc.auto) } } } // --- H1.2: branch/edge coverage deepening (test-only) --- // Group A — pure/exported functions (no fixtures, direct in-package calls). // TestGateSteps covers GateSteps (config.go:586-591), which was 0%: the // nil-chain non-nil-empty contract plus single/multi-step joining. func TestGateSteps(t *testing.T) { t.Run("nil chain yields non-nil empty", func(t *testing.T) { got := GateSteps(nil) if got == nil { t.Fatal("GateSteps(nil) = nil, want non-nil empty slice") } if len(got) != 0 { t.Errorf("GateSteps(nil) = %v, want empty", got) } }) cases := []struct { name string in [][]string want []string }{ {"single multi-arg step", [][]string{{"go", "vet", "./..."}}, []string{"go vet ./..."}}, {"two steps", [][]string{{"go", "vet"}, {"staticcheck", "./..."}}, []string{"go vet", "staticcheck ./..."}}, {"single word step", [][]string{{"make"}}, []string{"make"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := GateSteps(tc.in); !reflect.DeepEqual(got, tc.want) { t.Errorf("GateSteps(%v) = %v, want %v", tc.in, got, tc.want) } }) } } // TestValidateWorkspaceName covers the empty (config.go:647-649) and // not-clean (650-652) reject arms by calling the validator directly: Load // maps "" to DefaultWorkspace before validating, so the empty arm is only // reachable here. func TestValidateWorkspaceName(t *testing.T) { cases := []struct { name string wantErr bool }{ {"", true}, {"a//b", true}, {"./x", true}, {"/abs", true}, {"a/b", true}, {`a\b`, true}, {".", true}, {"..", true}, {".eeco", false}, {"workspace", false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if err := validateWorkspaceName(tc.name); (err != nil) != tc.wantErr { t.Errorf("validateWorkspaceName(%q) err = %v, wantErr = %v", tc.name, err, tc.wantErr) } }) } } // TestSlugUsername covers the space->dash arm (config.go:551-552) and the // drop/trim edges, including a result that trims to empty. func TestSlugUsername(t *testing.T) { cases := []struct { in string want string }{ {"Jane Doe", "Jane-Doe"}, {" ada ", "ada"}, {"a@b!c", "abc"}, {"...x...", "x"}, {"日本語", ""}, {".", ""}, {"", ""}, } for _, tc := range cases { t.Run(tc.in, func(t *testing.T) { if got := slugUsername(tc.in); got != tc.want { t.Errorf("slugUsername(%q) = %q, want %q", tc.in, got, tc.want) } }) } } // Group B — per-key config.local edge tables (driven via Load). This is // the "garbage in config.local -> documented default/error, never crash" // exit, one function per typed key not already covered. // TestLoad_ConfigLocalAICommand covers the ai_command split/empty arms // (config.go:760-762). func TestLoad_ConfigLocalAICommand(t *testing.T) { cases := []struct { name string body string want []string }{ {"multi-arg", "ai_command=my tool --flag\n", []string{"my", "tool", "--flag"}}, {"empty leaves nil", "ai_command=\n", nil}, {"single", "ai_command=solo\n", []string{"solo"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if !reflect.DeepEqual(cfg.AICommand, tc.want) { t.Errorf("AICommand = %v, want %v", cfg.AICommand, tc.want) } }) } } // TestLoad_ConfigLocalAIProvider covers the ai_provider passthrough // (config.go:778), including the floor invariant that an unknown value is // stored verbatim without error. func TestLoad_ConfigLocalAIProvider(t *testing.T) { cases := []struct { name string body string want string }{ {"cli", "ai_provider=cli\n", "cli"}, {"anthropic", "ai_provider=anthropic\n", "anthropic"}, {"empty", "ai_provider=\n", ""}, {"unknown tolerated", "ai_provider=galaxy-brain\n", "galaxy-brain"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatalf("Load: %v", err) } if cfg.AIProvider != tc.want { t.Errorf("AIProvider = %q, want %q", cfg.AIProvider, tc.want) } }) } } // TestLoad_ConfigLocalAIModel covers the ai_model passthrough // (config.go:782): an opaque identifier is stored verbatim, empty resets. func TestLoad_ConfigLocalAIModel(t *testing.T) { cases := []struct { name string body string want string }{ {"identifier", "ai_model=claude-x\n", "claude-x"}, {"empty", "ai_model=\n", ""}, {"opaque chars", "ai_model=anything/with:chars\n", "anything/with:chars"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatalf("Load: %v", err) } if cfg.AIModel != tc.want { t.Errorf("AIModel = %q, want %q", cfg.AIModel, tc.want) } }) } } // TestLoad_ConfigLocalAIAPIKeyEnv covers ai_api_key_env (config.go:786-790): // a name is taken verbatim, empty falls back to the default env-var name. func TestLoad_ConfigLocalAIAPIKeyEnv(t *testing.T) { cases := []struct { name string body string want string }{ {"custom name", "ai_api_key_env=MY_VAR\n", "MY_VAR"}, {"empty falls back to default", "ai_api_key_env=\n", DefaultAIAPIKeyEnv}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatalf("Load: %v", err) } if cfg.AIAPIKeyEnv != tc.want { t.Errorf("AIAPIKeyEnv = %q, want %q", cfg.AIAPIKeyEnv, tc.want) } }) } } // TestLoad_ConfigLocalSessionStartDocs covers session_start_docs accept // (config.go:809-810 empty-skip, 815 clean, 819 append) and reject // (812 absolute, 816 escape) arms. func TestLoad_ConfigLocalSessionStartDocs(t *testing.T) { t.Run("accepted with empty-skip and clean", func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ "session_start_docs=docs/a.md", "session_start_docs=b.md", "session_start_docs=", // empty value: skipped, no phantom entry "session_start_docs=sub/./c.md", "", }, "\n")) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } want := []string{"docs/a.md", "b.md", "sub/c.md"} if !reflect.DeepEqual(cfg.SessionStartDocs, want) { t.Errorf("SessionStartDocs = %v, want %v", cfg.SessionStartDocs, want) } }) for _, body := range []string{ "session_start_docs=/abs/x.md\n", "session_start_docs=..\n", "session_start_docs=../escape.md\n", "session_start_docs=sub/../../escape.md\n", } { t.Run("rejected "+body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } // TestLoad_ConfigLocalSessionFilesEmptyAndSlash covers session_files // empty-skip (config.go:828-829) and the backslash-prefix reject (837-839), // which is reachable on unix because filepath.IsAbs(`\x`) is false there // but the HasPrefix(`\`) guard still fires. func TestLoad_ConfigLocalSessionFilesEmptyAndSlash(t *testing.T) { t.Run("empty value skipped", func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", strings.Join([]string{ "session_files=CLAUDE.md", "session_files=", "", }, "\n")) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if len(cfg.SessionFiles) != 1 || cfg.SessionFiles[0] != "CLAUDE.md" { t.Errorf("SessionFiles = %v, want [CLAUDE.md]", cfg.SessionFiles) } }) t.Run("backslash-prefix rejected", func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", `session_files=\x`+"\n") if _, err := Load(root, ""); err == nil { t.Fatal(`expected error for session_files=\x`) } }) } // TestLoad_ConfigLocalSessionStartMailbox covers session_start_mailbox // accept/empty (config.go:849-850 disable, 858 clean) and reject // (851 absolute, 855 escape) arms. func TestLoad_ConfigLocalSessionStartMailbox(t *testing.T) { accept := []struct { name string body string want string }{ {"simple name", "session_start_mailbox=Inbox.md\n", "Inbox.md"}, {"empty disables", "session_start_mailbox=\n", ""}, {"clean relative", "session_start_mailbox=sub/./M.md\n", "sub/M.md"}, } for _, tc := range accept { t.Run(tc.name, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.SessionStartMailbox != tc.want { t.Errorf("SessionStartMailbox = %q, want %q", cfg.SessionStartMailbox, tc.want) } }) } for _, body := range []string{ "session_start_mailbox=/abs/M.md\n", "session_start_mailbox=..\n", "session_start_mailbox=../escape.md\n", } { t.Run("rejected "+body, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", body) if _, err := Load(root, ""); err == nil { t.Fatalf("expected error for %q", body) } }) } } // TestLoad_ConfigLocalSessionStartRoadmapGlob covers the // session_start_roadmap_glob passthrough (config.go:865): the glob is // stored verbatim, empty disables discovery. func TestLoad_ConfigLocalSessionStartRoadmapGlob(t *testing.T) { cases := []struct { name string body string want string }{ {"glob verbatim", "session_start_roadmap_glob=plan*.md\n", "plan*.md"}, {"empty disables", "session_start_roadmap_glob=\n", ""}, {"other glob verbatim", "session_start_roadmap_glob=ROADMAP-*.md\n", "ROADMAP-*.md"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", tc.body) cfg, err := Load(root, "") if err != nil { t.Fatalf("Load: %v", err) } if cfg.SessionStartRoadmapGlob != tc.want { t.Errorf("SessionStartRoadmapGlob = %q, want %q", cfg.SessionStartRoadmapGlob, tc.want) } }) } } // TestLoad_ConfigLocalInitDetectionThresholdEmpty covers the empty-value // arm of init_detection_threshold (config.go:1061-1063); the 0.85 and // malformed cases are covered elsewhere. func TestLoad_ConfigLocalInitDetectionThresholdEmpty(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, "config.local", "init_detection_threshold=\n") cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if cfg.InitDetectionThreshold != 0 { t.Errorf("InitDetectionThreshold = %v, want 0", cfg.InitDetectionThreshold) } } // Group C — nil guards + IsInitialized depth (local.go arms; the init.go // nil guards live in init_test.go). // TestWriteLocalKeys_NilConfig covers local.go:25-27. func TestWriteLocalKeys_NilConfig(t *testing.T) { if err := WriteLocalKeys(nil, map[string]string{"x": "y"}); err == nil { t.Fatal("WriteLocalKeys(nil, ...) = nil, want error") } } // TestWriteLocalKeys_EmptyMap covers local.go:28-30: an empty map is a // no-op that returns nil and creates no config.local. func TestWriteLocalKeys_EmptyMap(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } cfg, err := Load(root, "") if err != nil { t.Fatal(err) } if err := WriteLocalKeys(cfg, nil); err != nil { t.Fatalf("WriteLocalKeys(cfg, nil) = %v, want nil", err) } if _, err := os.Stat(filepath.Join(wsDir, LocalFilename)); !os.IsNotExist(err) { t.Errorf("config.local stat err = %v, want IsNotExist", err) } } // Group D — filesystem I/O error branches, NO seam (dir-/file-in-the-way). // Assertions target the package's own wrap text, never the OS errno, so // they are portable (EISDIR/ENOTDIR are both non-os.ErrNotExist). // TestApplyLocal_ErrorsWhenConfigLocalIsADirectory covers the applyLocal // ReadFile non-NotExist arm (config.go:687-688) and the Load read wrap // (638-639): the workspace stat succeeds (IsDir true), then ReadFile on a // directory fails. func TestApplyLocal_ErrorsWhenConfigLocalIsADirectory(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(wsDir, "config.local"), 0o755); err != nil { t.Fatal(err) } _, err := Load(root, "") if err == nil { t.Fatal("expected Load to error when config.local is a directory") } if !strings.Contains(err.Error(), "read config.local") { t.Errorf("error = %q, want it to contain %q", err.Error(), "read config.local") } } // TestWriteLocalKeys_ErrorsWhenConfigLocalIsADirectory covers // local.go:38-40 (ReadFile non-NotExist). WriteLocalKeys reads only // cfg.Workspace, so a minimal hand-built Config suffices. func TestWriteLocalKeys_ErrorsWhenConfigLocalIsADirectory(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(wsDir, LocalFilename), 0o755); err != nil { t.Fatal(err) } cfg := &Config{Workspace: wsDir} err := WriteLocalKeys(cfg, map[string]string{"automation": "auto"}) if err == nil { t.Fatal("expected WriteLocalKeys to error when config.local is a directory") } if !strings.Contains(err.Error(), "read config.local") { t.Errorf("error = %q, want it to contain %q", err.Error(), "read config.local") } } // TestWriteLocalKeys_PreservesRawNonKVLine covers the no-'=' raw // passthrough in WriteLocalKeys (local.go:52-54): a malformed line and a // comment survive an upsert untouched. func TestWriteLocalKeys_PreservesRawNonKVLine(t *testing.T) { root := newRepo(t) wsDir := filepath.Join(root, "tester", DefaultWorkspace) if err := os.MkdirAll(wsDir, 0o755); err != nil { t.Fatal(err) } write(t, wsDir, LocalFilename, strings.Join([]string{ "# a comment", "raw-line-without-equals", "stale_days=7", }, "\n")+"\n") // WriteLocalKeys reads only cfg.Workspace and parses config.local // itself; a hand-built Config avoids Load rejecting the malformed line. cfg := &Config{Workspace: wsDir} if err := WriteLocalKeys(cfg, map[string]string{"automation": "manual"}); err != nil { t.Fatal(err) } b, err := os.ReadFile(filepath.Join(wsDir, LocalFilename)) if err != nil { t.Fatal(err) } got := string(b) if !strings.Contains(got, "raw-line-without-equals") { t.Errorf("raw non-kv line not preserved:\n%s", got) } if !strings.Contains(got, "# a comment") { t.Errorf("comment not preserved:\n%s", got) } } // Group E — resolveUsername fallback + empty-username Init (the Init half // lives in init_test.go). // TestResolveUsername_FallbackWhenNoIdentity covers the final fallback // return (config.go:536). Every identity source is nulled: EECO_USERNAME // slugs to empty, USER/USERNAME are empty, and GIT_CONFIG_GLOBAL/SYSTEM // point at nonexistent files so the host's own git user.name cannot leak // (mirrors the H1.1 gitx isolation). The bare-.git fixture makes // gitx.UserName return ("", nil). func TestResolveUsername_FallbackWhenNoIdentity(t *testing.T) { t.Setenv(UsernameEnv, "!!!") // slugs to "" -> candidate skipped (overrides TestMain "tester") t.Setenv("USER", "") t.Setenv("USERNAME", "") t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(t.TempDir(), "nope-global")) t.Setenv("GIT_CONFIG_SYSTEM", filepath.Join(t.TempDir(), "nope-system")) root := newRepo(t) if got := resolveUsername(root); got != FallbackUsername { t.Errorf("resolveUsername = %q, want %q", got, FallbackUsername) } }