package walker import ( "os" "path/filepath" "runtime" "testing" "git.flo.fo/FLO/dirmd/internal/config" ) func writeFile(t *testing.T, dir, name, content string) { t.Helper() path := filepath.Join(dir, name) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatalf("mkdir for %s: %v", name, err) } if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("write %s: %v", name, err) } } func baseCfg(absRoot string) *config.Config { return &config.Config{ AbsRoot: absRoot, InputPath: absRoot, MaxSize: 1024 * 1024, MaxDepth: 20, Ignores: config.DefaultIgnores, Extensions: config.DefaultExts, } } func relPaths(entries []Entry) []string { paths := make([]string, 0, len(entries)) for _, e := range entries { paths = append(paths, e.RelPath) } return paths } func containsPath(paths []string, target string) bool { for _, p := range paths { if p == target { return true } } return false } func TestWalkDirectory_happy(t *testing.T) { root := t.TempDir() writeFile(t, root, "main.go", "package main\n") writeFile(t, root, "cmd/tool/main.go", "package main\n") writeFile(t, root, "cmd/tool/util.go", "package tool\n") writeFile(t, root, "internal/x/y/deep.go", "package y\n") result, err := Run(baseCfg(root)) if err != nil { t.Fatalf("Run: %v", err) } got := relPaths(result.Entries) want := []string{"main.go", "cmd/tool/main.go", "cmd/tool/util.go", "internal/x/y/deep.go"} if len(got) != len(want) { t.Fatalf("entries = %v; want %v", got, want) } for _, w := range want { if !containsPath(got, w) { t.Errorf("missing entry %q in %v", w, got) } } if result.AbsRoot != root { t.Errorf("AbsRoot = %q; want %q", result.AbsRoot, root) } if result.Cfg == nil { t.Error("Cfg not attached to result") } } func TestWalkDirectory_respects_filters(t *testing.T) { tests := []struct { name string setup func(t *testing.T) string must []string musts []string }{ { name: "default_ignores_skip_dirs_and_files", setup: func(t *testing.T) string { root := t.TempDir() writeFile(t, root, "main.go", "x\n") writeFile(t, root, ".git/config", "x\n") writeFile(t, root, "node_modules/pkg/index.js", "x\n") writeFile(t, root, "go.mod", "module x\n") writeFile(t, root, "docs/readme.txt", "x\n") return root }, must: []string{"main.go", "docs/readme.txt"}, musts: []string{".git/config", "node_modules/pkg/index.js", "go.mod"}, }, { name: "default_exts_skip_binary_types", setup: func(t *testing.T) string { root := t.TempDir() writeFile(t, root, "keep.go", "x\n") writeFile(t, root, "img.png", "x\n") writeFile(t, root, "arch.zip", "x\n") return root }, must: []string{"keep.go"}, musts: []string{"img.png", "arch.zip"}, }, { name: "hidden_files_and_dirs_skipped", setup: func(t *testing.T) string { root := t.TempDir() writeFile(t, root, "visible.go", "x\n") writeFile(t, root, ".hidden.go", "x\n") writeFile(t, root, ".github/workflows/ci.yml", "x\n") return root }, must: []string{"visible.go"}, musts: []string{".hidden.go", ".github/workflows/ci.yml"}, }, { name: "max_size_skips_large_files", setup: func(t *testing.T) string { root := t.TempDir() writeFile(t, root, "small.go", "x\n") writeFile(t, root, "large.go", "this content exceeds ten bytes\n") return root }, must: []string{"small.go"}, musts: []string{"large.go"}, }, { name: "omit_test_skips_go_tests", setup: func(t *testing.T) string { root := t.TempDir() writeFile(t, root, "main.go", "x\n") writeFile(t, root, "main_test.go", "x\n") writeFile(t, root, "tests/helper.py", "x\n") return root }, must: []string{"main.go"}, musts: []string{"main_test.go", "tests/helper.py"}, }, { name: "omit_md_keeps_readme_only", setup: func(t *testing.T) string { root := t.TempDir() writeFile(t, root, "README.md", "x\n") writeFile(t, root, "NOTES.md", "x\n") writeFile(t, root, "src/notes.md", "x\n") return root }, must: []string{"README.md"}, musts: []string{"NOTES.md", "src/notes.md"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { root := tt.setup(t) cfg := baseCfg(root) switch tt.name { case "max_size_skips_large_files": cfg.MaxSize = 10 case "omit_test_skips_go_tests": cfg.OmitTest = true case "omit_md_keeps_readme_only": cfg.OmitMd = true } result, err := Run(cfg) if err != nil { t.Fatalf("Run: %v", err) } got := relPaths(result.Entries) for _, w := range tt.must { if !containsPath(got, w) { t.Errorf("missing %q in %v", w, got) } } for _, w := range tt.musts { if containsPath(got, w) { t.Errorf("unexpected %q in %v", w, got) } } }) } } func TestWalkDirectory_custom_ignore(t *testing.T) { root := t.TempDir() writeFile(t, root, "main.go", "x\n") writeFile(t, root, "scratch/notes.txt", "x\n") cfg := baseCfg(root) cfg.Ignores = append([]string{}, config.DefaultIgnores...) cfg.Ignores = append(cfg.Ignores, "scratch") result, err := Run(cfg) if err != nil { t.Fatalf("Run: %v", err) } got := relPaths(result.Entries) if containsPath(got, "scratch/notes.txt") { t.Errorf("custom-ignored dir walked: %v", got) } if !containsPath(got, "main.go") { t.Errorf("missing main.go in %v", got) } } func TestWalkDirectory_max_depth(t *testing.T) { root := t.TempDir() writeFile(t, root, "a/b/c/d/e/deep.go", "x\n") writeFile(t, root, "a/shallow.go", "x\n") cfg := baseCfg(root) cfg.MaxDepth = 3 result, err := Run(cfg) if err != nil { t.Fatalf("Run: %v", err) } got := relPaths(result.Entries) if containsPath(got, "a/b/c/d/e/deep.go") { t.Errorf("file beyond max depth included: %v", got) } if !containsPath(got, "a/shallow.go") { t.Errorf("missing shallow file in %v", got) } } func TestWalkDirectory_symlink_skipped(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlinks unreliable on windows") } root := t.TempDir() target := filepath.Join(root, "real", "file.go") if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(target, []byte("x\n"), 0644); err != nil { t.Fatal(err) } if err := os.Symlink(filepath.Join(root, "real"), filepath.Join(root, "linkdir")); err != nil { t.Skipf("cannot create symlink: %v", err) } result, err := Run(baseCfg(root)) if err != nil { t.Fatalf("Run: %v", err) } got := relPaths(result.Entries) if containsPath(got, "linkdir/file.go") { t.Errorf("symlinked dir walked: %v", got) } if !containsPath(got, "real/file.go") { t.Errorf("missing real entry in %v", got) } } func TestWalkDirectory_instructions_read(t *testing.T) { root := t.TempDir() writeFile(t, root, "main.go", "x\n") writeFile(t, root, ".dirmd", "be helpful\n") cfg := baseCfg(root) cfg.IncludeInstructions = true result, err := Run(cfg) if err != nil { t.Fatalf("Run: %v", err) } if result.Instructions != "be helpful\n" { t.Errorf("Instructions = %q; want %q", result.Instructions, "be helpful\n") } } func TestWalkDirectory_instructions_not_read_by_default(t *testing.T) { root := t.TempDir() writeFile(t, root, "main.go", "x\n") writeFile(t, root, ".dirmd", "be helpful\n") result, err := Run(baseCfg(root)) if err != nil { t.Fatalf("Run: %v", err) } if result.Instructions != "" { t.Errorf("Instructions = %q; want empty", result.Instructions) } } func TestWalkDirectory_git_commit_outside_repo(t *testing.T) { root := t.TempDir() writeFile(t, root, "main.go", "x\n") result, err := Run(baseCfg(root)) if err != nil { t.Fatalf("Run: %v", err) } if result.Commit != nil { t.Errorf("Commit = %v; want nil outside a git repo", result.Commit) } } func TestProcessSingleFile(t *testing.T) { root := t.TempDir() writeFile(t, root, "main.go", "package main\n") cfg := baseCfg(filepath.Join(root, "main.go")) cfg.SingleFile = true result, err := Run(cfg) if err != nil { t.Fatalf("Run: %v", err) } if len(result.Entries) != 1 { t.Fatalf("entries = %v; want single entry", result.Entries) } if result.Entries[0].RelPath != "main.go" { t.Errorf("RelPath = %q; want %q", result.Entries[0].RelPath, "main.go") } if result.AbsRoot != root { t.Errorf("AbsRoot = %q; want %q", result.AbsRoot, root) } if !result.SingleFile { t.Error("SingleFile not set") } } func TestProcessSingleFile_filtered_out(t *testing.T) { root := t.TempDir() writeFile(t, root, "image.png", "x\n") cfg := baseCfg(filepath.Join(root, "image.png")) cfg.SingleFile = true _, err := Run(cfg) if err == nil { t.Fatal("expected error for filtered single file") } } func TestProcessSingleFile_missing(t *testing.T) { cfg := baseCfg("/nonexistent/path/file.go") cfg.SingleFile = true _, err := Run(cfg) if err == nil { t.Fatal("expected error for missing file") } }