changedir_test.go

  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}