package workspace import ( "context" "os" "os/exec" "path/filepath" "slices" "testing" ) // seedRepo creates a tiny git repo with one tracked file, one // gitignored file, and one untracked-non-ignored file. Returns the // repo root path. Skips the test if git isn't on PATH (unusual for // a dev machine, but polite). func seedRepo(t *testing.T) string { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skipf("git not on PATH: %v", err) } dir := t.TempDir() run := func(args ...string) { t.Helper() cmd := exec.Command(args[0], args[1:]...) cmd.Dir = dir // Isolate from the ambient user config so commits don't need // a global user.name/user.email. Also disable GPG signing. cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t", "GIT_CONFIG_GLOBAL=/dev/null", ) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%v: %v\n%s", args, err, out) } } writeFile := func(relPath, content string) { t.Helper() if err := os.WriteFile(filepath.Join(dir, relPath), []byte(content), 0o644); err != nil { t.Fatal(err) } } run("git", "init", "-q", "-b", "main") run("git", "config", "commit.gpgsign", "false") writeFile(".gitignore", "ignored.log\n") writeFile("README.md", "hello\n") run("git", "add", ".gitignore", "README.md") run("git", "commit", "-q", "-m", "init") // A tracked file AFTER the first commit so ls-files picks it up. // A gitignored file so --exclude-standard filters it. // An untracked non-ignored file so the flag matters. writeFile("src.go", "package main\n") run("git", "add", "src.go") run("git", "commit", "-q", "-m", "src") writeFile("ignored.log", "noisy\n") writeFile("SECRETS.env", "TOKEN=abc\n") return dir } func TestListOverlayPaths_TrackedOnlyByDefault(t *testing.T) { repo := seedRepo(t) got, err := ListOverlayPaths(context.Background(), repo, false) if err != nil { t.Fatalf("ListOverlayPaths: %v", err) } want := []string{".gitignore", "README.md", "src.go"} if !slices.Equal(got, want) { t.Fatalf("got %v, want %v (untracked SECRETS.env must be excluded; gitignored ignored.log must always be excluded)", got, want) } } func TestListOverlayPaths_IncludeUntracked(t *testing.T) { repo := seedRepo(t) got, err := ListOverlayPaths(context.Background(), repo, true) if err != nil { t.Fatalf("ListOverlayPaths: %v", err) } want := []string{".gitignore", "README.md", "SECRETS.env", "src.go"} if !slices.Equal(got, want) { t.Fatalf("got %v, want %v", got, want) } // gitignored files must stay out even when untracked is included. for _, p := range got { if p == "ignored.log" { t.Fatalf("gitignored file leaked into overlay: %v", got) } } } func TestCountUntrackedPaths(t *testing.T) { repo := seedRepo(t) count, err := CountUntrackedPaths(context.Background(), repo) if err != nil { t.Fatalf("CountUntrackedPaths: %v", err) } if count != 1 { t.Fatalf("count = %d, want 1 (only SECRETS.env; ignored.log is gitignored)", count) } }