1package tools
2
3import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12func TestManageWorktreesListsCleanupEligibility(t *testing.T) {
13 if _, err := exec.LookPath("git"); err != nil {
14 t.Skip("git is required")
15 }
16 repo := initWorktreeTestRepo(t)
17 mergedPath := filepath.Join(t.TempDir(), "merged")
18 gitTestOutput(t, repo, "branch", "merged")
19 gitTestOutput(t, repo, "worktree", "add", mergedPath, "merged")
20 dirtyPath := filepath.Join(t.TempDir(), "dirty")
21 gitTestOutput(t, repo, "worktree", "add", "-b", "dirty", dirtyPath)
22 if err := os.WriteFile(filepath.Join(dirtyPath, "dirty.txt"), []byte("dirty"), 0o644); err != nil {
23 t.Fatal(err)
24 }
25
26 tool := newManageWorktreesTestTool(repo)
27 result, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"action": "list"}), nil)
28 if err != nil {
29 t.Fatal(err)
30 }
31 got := resultText(result)
32 for _, want := range []string{"repository worktrees", "merged", "cleanup eligible", "dirty", "uncommitted changes", "primary worktree"} {
33 if !strings.Contains(got, want) {
34 t.Fatalf("result missing %q:\n%s", want, got)
35 }
36 }
37}
38
39func TestManageWorktreesCleanupRemovesOnlyExplicitCleanMergedWorktree(t *testing.T) {
40 if _, err := exec.LookPath("git"); err != nil {
41 t.Skip("git is required")
42 }
43 repo := initWorktreeTestRepo(t)
44 path := filepath.Join(t.TempDir(), "merged")
45 gitTestOutput(t, repo, "branch", "merged")
46 gitTestOutput(t, repo, "worktree", "add", path, "merged")
47 tool := newManageWorktreesTestTool(repo)
48 args := mustJSON(t, map[string]any{"action": "cleanup", "paths": []string{path}})
49
50 if _, err := tool.Preview(context.Background(), args); err != nil {
51 t.Fatal(err)
52 }
53 if _, err := os.Stat(path); err != nil {
54 t.Fatalf("preview removed worktree: %v", err)
55 }
56 if _, err := tool.Execute(context.Background(), args, nil); err != nil {
57 t.Fatal(err)
58 }
59 if _, err := os.Stat(path); !os.IsNotExist(err) {
60 t.Fatalf("worktree remains: %v", err)
61 }
62 if got := gitTestOutput(t, repo, "show-ref", "--verify", "refs/heads/merged"); got == "" {
63 t.Fatal("cleanup unexpectedly removed branch")
64 }
65}
66
67func TestManageWorktreesRejectsDirtyCleanup(t *testing.T) {
68 if _, err := exec.LookPath("git"); err != nil {
69 t.Skip("git is required")
70 }
71 repo := initWorktreeTestRepo(t)
72 path := filepath.Join(t.TempDir(), "dirty")
73 gitTestOutput(t, repo, "worktree", "add", "-b", "dirty", path)
74 if err := os.WriteFile(filepath.Join(path, "dirty.txt"), []byte("dirty"), 0o644); err != nil {
75 t.Fatal(err)
76 }
77 tool := newManageWorktreesTestTool(repo)
78 _, err := tool.Preview(context.Background(), mustJSON(t, map[string]any{"action": "cleanup", "paths": []string{path}}))
79 if err == nil || !strings.Contains(err.Error(), "uncommitted changes") {
80 t.Fatalf("error = %v, want dirty-worktree rejection", err)
81 }
82}
83
84func TestWorktreeInventoryContextReportsRepositoryWorktrees(t *testing.T) {
85 if _, err := exec.LookPath("git"); err != nil {
86 t.Skip("git is required")
87 }
88 repo := initWorktreeTestRepo(t)
89 path := filepath.Join(t.TempDir(), "merged")
90 gitTestOutput(t, repo, "branch", "merged")
91 gitTestOutput(t, repo, "worktree", "add", path, "merged")
92
93 contextText := WorktreeInventoryContext(context.Background(), repo)
94 wantPath, err := canonicalOrParent(path)
95 if err != nil {
96 t.Fatal(err)
97 }
98 for _, want := range []string{"[Repository worktrees]", filepath.ToSlash(wantPath), "worktree action=list"} {
99 if !strings.Contains(contextText, want) {
100 t.Fatalf("context missing %q:\n%s", want, contextText)
101 }
102 }
103}
104
105func TestManageWorktreesRejectsIgnoredFilesCleanup(t *testing.T) {
106 if _, err := exec.LookPath("git"); err != nil {
107 t.Skip("git is required")
108 }
109 repo := initWorktreeTestRepo(t)
110 path := filepath.Join(t.TempDir(), "ignored")
111 gitTestOutput(t, repo, "worktree", "add", "-b", "ignored", path)
112 if err := os.WriteFile(filepath.Join(path, ".gitignore"), []byte("local.env\n"), 0o644); err != nil {
113 t.Fatal(err)
114 }
115 gitTestOutput(t, path, "add", ".gitignore")
116 gitTestOutput(t, path, "commit", "-m", "ignore local file")
117 gitTestOutput(t, repo, "merge", "--ff-only", "ignored")
118 if err := os.WriteFile(filepath.Join(path, "local.env"), []byte("private"), 0o600); err != nil {
119 t.Fatal(err)
120 }
121 tool := newManageWorktreesTestTool(repo)
122 _, err := tool.Preview(context.Background(), mustJSON(t, map[string]any{"action": "cleanup", "paths": []string{path}}))
123 if err == nil || !strings.Contains(err.Error(), "uncommitted changes") {
124 t.Fatalf("error = %v, want ignored-file rejection", err)
125 }
126}
127
128func TestManageWorktreesRejectsCleanupWithoutExplicitPaths(t *testing.T) {
129 if _, err := exec.LookPath("git"); err != nil {
130 t.Skip("git is required")
131 }
132 repo := initWorktreeTestRepo(t)
133 tool := newManageWorktreesTestTool(repo)
134 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"action": "cleanup"}), nil)
135 if err == nil || !strings.Contains(err.Error(), "explicit path") {
136 t.Fatalf("error = %v, want explicit-path rejection", err)
137 }
138}
139
140func newManageWorktreesTestTool(cwd string) *ManageWorktreesTool {
141 return &ManageWorktreesTool{CWD: cwd, Sandbox: NewSandbox(cwd)}
142}