1package claudetool
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "testing"
9)
10
11func TestChangeDirTool(t *testing.T) {
12 // Create a temp directory structure
13 tmpDir := t.TempDir()
14 subDir := filepath.Join(tmpDir, "subdir")
15 if err := os.Mkdir(subDir, 0o755); err != nil {
16 t.Fatal(err)
17 }
18
19 wd := NewMutableWorkingDir(tmpDir)
20 tool := &ChangeDirTool{WorkingDir: wd}
21
22 t.Run("change to absolute path", func(t *testing.T) {
23 // Reset
24 wd.Set(tmpDir)
25
26 input, _ := json.Marshal(changeDirInput{Path: subDir})
27 result := tool.Run(context.Background(), input)
28
29 if result.Error != nil {
30 t.Fatalf("unexpected error: %v", result.Error)
31 }
32
33 if wd.Get() != subDir {
34 t.Errorf("expected working dir %q, got %q", subDir, wd.Get())
35 }
36 })
37
38 t.Run("change to relative path", func(t *testing.T) {
39 // Reset
40 wd.Set(tmpDir)
41
42 input, _ := json.Marshal(changeDirInput{Path: "subdir"})
43 result := tool.Run(context.Background(), input)
44
45 if result.Error != nil {
46 t.Fatalf("unexpected error: %v", result.Error)
47 }
48
49 if wd.Get() != subDir {
50 t.Errorf("expected working dir %q, got %q", subDir, wd.Get())
51 }
52 })
53
54 t.Run("change to parent directory", func(t *testing.T) {
55 wd.Set(subDir)
56
57 input, _ := json.Marshal(changeDirInput{Path: ".."})
58 result := tool.Run(context.Background(), input)
59
60 if result.Error != nil {
61 t.Fatalf("unexpected error: %v", result.Error)
62 }
63
64 if wd.Get() != tmpDir {
65 t.Errorf("expected working dir %q, got %q", tmpDir, wd.Get())
66 }
67 })
68
69 t.Run("error on non-existent path", func(t *testing.T) {
70 wd.Set(tmpDir)
71
72 input, _ := json.Marshal(changeDirInput{Path: "/nonexistent/path"})
73 result := tool.Run(context.Background(), input)
74
75 if result.Error == nil {
76 t.Fatal("expected error for non-existent path")
77 }
78 })
79
80 t.Run("error on file path", func(t *testing.T) {
81 // Create a file
82 filePath := filepath.Join(tmpDir, "file.txt")
83 if err := os.WriteFile(filePath, []byte("test"), 0o644); err != nil {
84 t.Fatal(err)
85 }
86
87 wd.Set(tmpDir)
88
89 input, _ := json.Marshal(changeDirInput{Path: filePath})
90 result := tool.Run(context.Background(), input)
91
92 if result.Error == nil {
93 t.Fatal("expected error for file path")
94 }
95 })
96
97 t.Run("OnChange callback is called", func(t *testing.T) {
98 wd.Set(tmpDir)
99
100 var callbackDir string
101 toolWithCallback := &ChangeDirTool{
102 WorkingDir: wd,
103 OnChange: func(newDir string) {
104 callbackDir = newDir
105 },
106 }
107
108 input, _ := json.Marshal(changeDirInput{Path: subDir})
109 result := toolWithCallback.Run(context.Background(), input)
110
111 if result.Error != nil {
112 t.Fatalf("unexpected error: %v", result.Error)
113 }
114
115 if callbackDir != subDir {
116 t.Errorf("expected callback dir %q, got %q", subDir, callbackDir)
117 }
118 })
119}
120
121func TestChangeDirWithBash(t *testing.T) {
122 // Create a temp directory structure
123 tmpDir := t.TempDir()
124 subDir := filepath.Join(tmpDir, "subdir")
125 if err := os.Mkdir(subDir, 0o755); err != nil {
126 t.Fatal(err)
127 }
128
129 // Create a file in subdir
130 testFile := filepath.Join(subDir, "test.txt")
131 if err := os.WriteFile(testFile, []byte("hello"), 0o644); err != nil {
132 t.Fatal(err)
133 }
134
135 wd := NewMutableWorkingDir(tmpDir)
136 changeDirTool := &ChangeDirTool{WorkingDir: wd}
137 bashTool := &BashTool{WorkingDir: wd}
138
139 ctx := context.Background()
140
141 // Run pwd to verify starting directory
142 input, _ := json.Marshal(bashInput{Command: "pwd"})
143 result := bashTool.Run(ctx, input)
144 if result.Error != nil {
145 t.Fatalf("bash pwd failed: %v", result.Error)
146 }
147
148 // Change directory
149 cdInput, _ := json.Marshal(changeDirInput{Path: subDir})
150 result = changeDirTool.Run(ctx, cdInput)
151 if result.Error != nil {
152 t.Fatalf("change_dir failed: %v", result.Error)
153 }
154
155 // Run ls - should now see test.txt
156 result = bashTool.Run(ctx, json.RawMessage(`{"command": "ls"}`))
157 if result.Error != nil {
158 t.Fatalf("bash ls failed: %v", result.Error)
159 }
160
161 // Verify we can see test.txt (indicating we're in subdir)
162 if len(result.LLMContent) == 0 {
163 t.Fatal("expected output from ls")
164 }
165 output := result.LLMContent[0].Text
166 if output == "" {
167 t.Fatal("expected non-empty output from ls")
168 }
169 // The output should contain "test.txt"
170 if !contains(output, "test.txt") {
171 t.Errorf("expected ls output to contain 'test.txt', got: %q", output)
172 }
173}
174
175func contains(s, substr string) bool {
176 for i := 0; i <= len(s)-len(substr); i++ {
177 if s[i:i+len(substr)] == substr {
178 return true
179 }
180 }
181 return false
182}
183
184func TestBashToolMissingWorkingDir(t *testing.T) {
185 // Create a temp directory, then remove it
186 tmpDir := t.TempDir()
187 subDir := filepath.Join(tmpDir, "subdir")
188 if err := os.Mkdir(subDir, 0o755); err != nil {
189 t.Fatal(err)
190 }
191
192 wd := NewMutableWorkingDir(subDir)
193 bashTool := &BashTool{WorkingDir: wd}
194
195 // Remove the directory
196 if err := os.RemoveAll(subDir); err != nil {
197 t.Fatal(err)
198 }
199
200 // Try to run a command - should get a clear error
201 input, _ := json.Marshal(bashInput{Command: "ls"})
202 result := bashTool.Run(context.Background(), input)
203
204 if result.Error == nil {
205 t.Fatal("expected error when working directory doesn't exist")
206 }
207
208 errStr := result.Error.Error()
209 if !contains(errStr, "working directory does not exist") {
210 t.Errorf("expected error about missing working directory, got: %s", errStr)
211 }
212 if !contains(errStr, "change_dir") {
213 t.Errorf("expected error to mention change_dir tool, got: %s", errStr)
214 }
215}
216
217func TestChangeDirTool_Method(t *testing.T) {
218 wd := NewMutableWorkingDir("/test")
219 tool := &ChangeDirTool{WorkingDir: wd}
220 llmTool := tool.Tool()
221
222 if llmTool == nil {
223 t.Fatal("Tool() returned nil")
224 }
225
226 if llmTool.Name != changeDirName {
227 t.Errorf("expected name %q, got %q", changeDirName, llmTool.Name)
228 }
229
230 if llmTool.Description != changeDirDescription {
231 t.Errorf("expected description %q, got %q", changeDirDescription, llmTool.Description)
232 }
233
234 if llmTool.Run == nil {
235 t.Error("Run function not set")
236 }
237}