Resolved conflicts: - pkg/heartbeat/service.go: merged both 'started' field and 'onHeartbeatWithTools' - pkg/tools/edit.go: use validatePath() with ToolResult return - pkg/tools/filesystem.go: fixed return values to use ToolResult - cmd/picoclaw/main.go: kept active setupCronTool, fixed toolsPkg import - pkg/tools/cron.go: fixed Execute return value handling Fixed tests for new function signatures (NewEditFileTool, NewAppendFileTool, NewExecTool) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
290 lines
7.9 KiB
Go
290 lines
7.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestEditTool_EditFile_Success verifies successful file editing
|
|
func TestEditTool_EditFile_Success(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0644)
|
|
|
|
tool := NewEditFileTool(tmpDir, true)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": testFile,
|
|
"old_text": "World",
|
|
"new_text": "Universe",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Success should not be an error
|
|
if result.IsError {
|
|
t.Errorf("Expected success, got IsError=true: %s", result.ForLLM)
|
|
}
|
|
|
|
// Should return SilentResult
|
|
if !result.Silent {
|
|
t.Errorf("Expected Silent=true for EditFile, got false")
|
|
}
|
|
|
|
// ForUser should be empty (silent result)
|
|
if result.ForUser != "" {
|
|
t.Errorf("Expected ForUser to be empty for SilentResult, got: %s", result.ForUser)
|
|
}
|
|
|
|
// Verify file was actually edited
|
|
content, err := os.ReadFile(testFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read edited file: %v", err)
|
|
}
|
|
contentStr := string(content)
|
|
if !strings.Contains(contentStr, "Hello Universe") {
|
|
t.Errorf("Expected file to contain 'Hello Universe', got: %s", contentStr)
|
|
}
|
|
if strings.Contains(contentStr, "Hello World") {
|
|
t.Errorf("Expected 'Hello World' to be replaced, got: %s", contentStr)
|
|
}
|
|
}
|
|
|
|
// TestEditTool_EditFile_NotFound verifies error handling for non-existent file
|
|
func TestEditTool_EditFile_NotFound(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "nonexistent.txt")
|
|
|
|
tool := NewEditFileTool(tmpDir, true)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": testFile,
|
|
"old_text": "old",
|
|
"new_text": "new",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error for non-existent file")
|
|
}
|
|
|
|
// Should mention file not found
|
|
if !strings.Contains(result.ForLLM, "not found") && !strings.Contains(result.ForUser, "not found") {
|
|
t.Errorf("Expected 'file not found' message, got ForLLM: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
// TestEditTool_EditFile_OldTextNotFound verifies error when old_text doesn't exist
|
|
func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
os.WriteFile(testFile, []byte("Hello World"), 0644)
|
|
|
|
tool := NewEditFileTool(tmpDir, true)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": testFile,
|
|
"old_text": "Goodbye",
|
|
"new_text": "Hello",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when old_text not found")
|
|
}
|
|
|
|
// Should mention old_text not found
|
|
if !strings.Contains(result.ForLLM, "not found") && !strings.Contains(result.ForUser, "not found") {
|
|
t.Errorf("Expected 'not found' message, got ForLLM: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
// TestEditTool_EditFile_MultipleMatches verifies error when old_text appears multiple times
|
|
func TestEditTool_EditFile_MultipleMatches(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
os.WriteFile(testFile, []byte("test test test"), 0644)
|
|
|
|
tool := NewEditFileTool(tmpDir, true)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": testFile,
|
|
"old_text": "test",
|
|
"new_text": "done",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when old_text appears multiple times")
|
|
}
|
|
|
|
// Should mention multiple occurrences
|
|
if !strings.Contains(result.ForLLM, "times") && !strings.Contains(result.ForUser, "times") {
|
|
t.Errorf("Expected 'multiple times' message, got ForLLM: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
// TestEditTool_EditFile_OutsideAllowedDir verifies error when path is outside allowed directory
|
|
func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
otherDir := t.TempDir()
|
|
testFile := filepath.Join(otherDir, "test.txt")
|
|
os.WriteFile(testFile, []byte("content"), 0644)
|
|
|
|
tool := NewEditFileTool(tmpDir, true) // Restrict to tmpDir
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": testFile,
|
|
"old_text": "content",
|
|
"new_text": "new",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when path is outside allowed directory")
|
|
}
|
|
|
|
// Should mention outside allowed directory
|
|
if !strings.Contains(result.ForLLM, "outside") && !strings.Contains(result.ForUser, "outside") {
|
|
t.Errorf("Expected 'outside allowed' message, got ForLLM: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
// TestEditTool_EditFile_MissingPath verifies error handling for missing path
|
|
func TestEditTool_EditFile_MissingPath(t *testing.T) {
|
|
tool := NewEditFileTool("", false)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"old_text": "old",
|
|
"new_text": "new",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when path is missing")
|
|
}
|
|
}
|
|
|
|
// TestEditTool_EditFile_MissingOldText verifies error handling for missing old_text
|
|
func TestEditTool_EditFile_MissingOldText(t *testing.T) {
|
|
tool := NewEditFileTool("", false)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": "/tmp/test.txt",
|
|
"new_text": "new",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when old_text is missing")
|
|
}
|
|
}
|
|
|
|
// TestEditTool_EditFile_MissingNewText verifies error handling for missing new_text
|
|
func TestEditTool_EditFile_MissingNewText(t *testing.T) {
|
|
tool := NewEditFileTool("", false)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": "/tmp/test.txt",
|
|
"old_text": "old",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when new_text is missing")
|
|
}
|
|
}
|
|
|
|
// TestEditTool_AppendFile_Success verifies successful file appending
|
|
func TestEditTool_AppendFile_Success(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
os.WriteFile(testFile, []byte("Initial content"), 0644)
|
|
|
|
tool := NewAppendFileTool("", false)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": testFile,
|
|
"content": "\nAppended content",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Success should not be an error
|
|
if result.IsError {
|
|
t.Errorf("Expected success, got IsError=true: %s", result.ForLLM)
|
|
}
|
|
|
|
// Should return SilentResult
|
|
if !result.Silent {
|
|
t.Errorf("Expected Silent=true for AppendFile, got false")
|
|
}
|
|
|
|
// ForUser should be empty (silent result)
|
|
if result.ForUser != "" {
|
|
t.Errorf("Expected ForUser to be empty for SilentResult, got: %s", result.ForUser)
|
|
}
|
|
|
|
// Verify content was actually appended
|
|
content, err := os.ReadFile(testFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read file: %v", err)
|
|
}
|
|
contentStr := string(content)
|
|
if !strings.Contains(contentStr, "Initial content") {
|
|
t.Errorf("Expected original content to remain, got: %s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "Appended content") {
|
|
t.Errorf("Expected appended content, got: %s", contentStr)
|
|
}
|
|
}
|
|
|
|
// TestEditTool_AppendFile_MissingPath verifies error handling for missing path
|
|
func TestEditTool_AppendFile_MissingPath(t *testing.T) {
|
|
tool := NewAppendFileTool("", false)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"content": "test",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when path is missing")
|
|
}
|
|
}
|
|
|
|
// TestEditTool_AppendFile_MissingContent verifies error handling for missing content
|
|
func TestEditTool_AppendFile_MissingContent(t *testing.T) {
|
|
tool := NewAppendFileTool("", false)
|
|
ctx := context.Background()
|
|
args := map[string]interface{}{
|
|
"path": "/tmp/test.txt",
|
|
}
|
|
|
|
result := tool.Execute(ctx, args)
|
|
|
|
// Should return error result
|
|
if !result.IsError {
|
|
t.Errorf("Expected error when content is missing")
|
|
}
|
|
}
|