package file import ( "context" "os" "path/filepath" "strings" "testing" "github.com/enmanuel/agents/internal/config" ) func TestWriteFile_CreatesNewFile(t *testing.T) { tmp := t.TempDir() cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: false} tool := NewWriteFile(cfg) target := filepath.Join(tmp, "new.txt") result := tool.Exec(context.Background(), map[string]any{ "path": target, "content": "hello world", }) if result.Err != nil { t.Fatalf("expected success, got: %v", result.Err) } data, err := os.ReadFile(target) if err != nil { t.Fatalf("file not created: %v", err) } if string(data) != "hello world" { t.Fatalf("expected 'hello world', got %q", string(data)) } if !strings.Contains(result.Output, "11 bytes") { t.Fatalf("expected output mentioning bytes, got %q", result.Output) } } func TestWriteFile_RejectsReadOnly(t *testing.T) { tmp := t.TempDir() cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: true} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": filepath.Join(tmp, "test.txt"), "content": "data", }) if result.Err == nil { t.Fatal("expected error when ReadOnly is true") } if !strings.Contains(result.Err.Error(), "read_only") { t.Fatalf("expected read_only error, got: %v", result.Err) } } func TestWriteFile_RejectsPathOutsideAllowed(t *testing.T) { tmp := t.TempDir() cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: false} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": "/etc/test.txt", "content": "data", }) if result.Err == nil { t.Fatal("expected error for path outside allowed") } } func TestWriteFile_RejectsContentOver1MB(t *testing.T) { tmp := t.TempDir() cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: false} tool := NewWriteFile(cfg) bigContent := strings.Repeat("x", maxWriteSize+1) result := tool.Exec(context.Background(), map[string]any{ "path": filepath.Join(tmp, "big.txt"), "content": bigContent, }) if result.Err == nil { t.Fatal("expected error for content exceeding 1 MB") } if !strings.Contains(result.Err.Error(), "1 MB") { t.Fatalf("expected 1 MB error, got: %v", result.Err) } } func TestWriteFile_CreatesParentDirectories(t *testing.T) { tmp := t.TempDir() cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: false} tool := NewWriteFile(cfg) target := filepath.Join(tmp, "sub", "dir", "file.txt") result := tool.Exec(context.Background(), map[string]any{ "path": target, "content": "nested", }) if result.Err != nil { t.Fatalf("expected success, got: %v", result.Err) } data, err := os.ReadFile(target) if err != nil { t.Fatalf("file not created: %v", err) } if string(data) != "nested" { t.Fatalf("expected 'nested', got %q", string(data)) } } func TestWriteFile_OverwritesExistingFile(t *testing.T) { tmp := t.TempDir() target := filepath.Join(tmp, "exists.txt") os.WriteFile(target, []byte("old"), 0644) cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: false} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": target, "content": "new content", }) if result.Err != nil { t.Fatalf("expected success, got: %v", result.Err) } data, _ := os.ReadFile(target) if string(data) != "new content" { t.Fatalf("expected 'new content', got %q", string(data)) } } func TestWriteFile_PathTraversal(t *testing.T) { tmp := t.TempDir() cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: false} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": filepath.Join(tmp, "..", "..", "etc", "evil.txt"), "content": "data", }) if result.Err == nil { t.Fatal("expected error for path traversal") } } func TestWriteFile_SymlinkEscape(t *testing.T) { tmp := t.TempDir() link := filepath.Join(tmp, "escape") os.Symlink("/tmp", link) cfg := config.FileOpsCfg{AllowedPaths: []string{tmp}, ReadOnly: false} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": filepath.Join(link, "evil.txt"), "content": "data", }) if result.Err == nil { t.Fatal("expected error for symlink escape") } } func TestWriteFile_EmptyPath(t *testing.T) { cfg := config.FileOpsCfg{AllowedPaths: []string{"/tmp"}, ReadOnly: false} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": "", "content": "data", }) if result.Err == nil { t.Fatal("expected error for empty path") } } func TestWriteFile_EmptyContent(t *testing.T) { cfg := config.FileOpsCfg{AllowedPaths: []string{"/tmp"}, ReadOnly: false} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": "/tmp/test.txt", "content": "", }) if result.Err == nil { t.Fatal("expected error for empty content") } } func TestWriteFile_DenyByDefault(t *testing.T) { cfg := config.FileOpsCfg{AllowedPaths: []string{}, ReadOnly: false} tool := NewWriteFile(cfg) result := tool.Exec(context.Background(), map[string]any{ "path": "/tmp/test.txt", "content": "data", }) if result.Err == nil { t.Fatal("expected error when AllowedPaths is empty") } }