Core features: - Microkernel architecture with Actor model - Session management with JSONL persistence - Tool system (5 built-in tools) - Skill system with SKILL.md parsing - Sandbox security execution - Ollama integration with gemma4:e4b - Prompt-based tool calling (compatible with native function calling) - REPL interface 11 packages, all tests passing
400 lines
9.7 KiB
Go
400 lines
9.7 KiB
Go
package tool
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/orca/orca/pkg/sandbox"
|
|
)
|
|
|
|
func TestExecTool(t *testing.T) {
|
|
sb := sandbox.NewProcessSandbox()
|
|
execT := NewExecTool(sb)
|
|
|
|
if execT.Name() != "exec" {
|
|
t.Errorf("expected name 'exec', got %q", execT.Name())
|
|
}
|
|
if execT.Description() == "" {
|
|
t.Error("expected non-empty description")
|
|
}
|
|
|
|
params := execT.Parameters()
|
|
if _, ok := params["command"]; !ok {
|
|
t.Error("expected 'command' parameter")
|
|
}
|
|
}
|
|
|
|
func TestExecToolExecute(t *testing.T) {
|
|
sb := sandbox.NewProcessSandbox()
|
|
execT := NewExecTool(sb)
|
|
ctx := context.Background()
|
|
|
|
result, err := execT.Execute(ctx, map[string]interface{}{
|
|
"command": "echo hello",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Errorf("expected success, got error: %s", result.Error)
|
|
}
|
|
|
|
data := result.Data.(map[string]interface{})
|
|
stdout := data["stdout"].(string)
|
|
if strings.TrimSpace(stdout) != "hello" {
|
|
t.Errorf("expected stdout 'hello', got %q", stdout)
|
|
}
|
|
}
|
|
|
|
func TestExecToolMissingCommand(t *testing.T) {
|
|
sb := sandbox.NewProcessSandbox()
|
|
execT := NewExecTool(sb)
|
|
ctx := context.Background()
|
|
|
|
result, err := execT.Execute(ctx, map[string]interface{}{})
|
|
if err != nil {
|
|
t.Fatalf("Execute should not error for invalid args: %v", err)
|
|
}
|
|
if result.Success {
|
|
t.Error("expected failure for missing command")
|
|
}
|
|
if !strings.Contains(result.Error, "command") {
|
|
t.Errorf("error should mention 'command', got: %s", result.Error)
|
|
}
|
|
}
|
|
|
|
func TestReadFileTool(t *testing.T) {
|
|
readT := NewReadFileTool()
|
|
|
|
if readT.Name() != "read_file" {
|
|
t.Errorf("expected name 'read_file', got %q", readT.Name())
|
|
}
|
|
}
|
|
|
|
func TestReadFileToolExecute(t *testing.T) {
|
|
// Create a temp file
|
|
tmpFile, err := os.CreateTemp("", "orca-test-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp file: %v", err)
|
|
}
|
|
content := "test content\nline 2\n"
|
|
if _, err := tmpFile.WriteString(content); err != nil {
|
|
t.Fatalf("failed to write temp file: %v", err)
|
|
}
|
|
tmpFile.Close()
|
|
defer os.Remove(tmpFile.Name())
|
|
|
|
readT := NewReadFileTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := readT.Execute(ctx, map[string]interface{}{
|
|
"path": tmpFile.Name(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Errorf("expected success, got error: %s", result.Error)
|
|
}
|
|
|
|
data := result.Data.(map[string]interface{})
|
|
gotContent := data["content"].(string)
|
|
if gotContent != content {
|
|
t.Errorf("expected content %q, got %q", content, gotContent)
|
|
}
|
|
}
|
|
|
|
func TestReadFileToolMissingPath(t *testing.T) {
|
|
readT := NewReadFileTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := readT.Execute(ctx, map[string]interface{}{})
|
|
if err != nil {
|
|
t.Fatalf("Execute should not error for invalid args: %v", err)
|
|
}
|
|
if result.Success {
|
|
t.Error("expected failure for missing path")
|
|
}
|
|
}
|
|
|
|
func TestReadFileToolNonexistent(t *testing.T) {
|
|
readT := NewReadFileTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := readT.Execute(ctx, map[string]interface{}{
|
|
"path": "/nonexistent/path/that/does/not/exist.txt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute should not error for missing file: %v", err)
|
|
}
|
|
if result.Success {
|
|
t.Error("expected failure for nonexistent file")
|
|
}
|
|
}
|
|
|
|
func TestWriteFileTool(t *testing.T) {
|
|
writeT := NewWriteFileTool()
|
|
|
|
if writeT.Name() != "write_file" {
|
|
t.Errorf("expected name 'write_file', got %q", writeT.Name())
|
|
}
|
|
}
|
|
|
|
func TestWriteFileToolExecute(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "orca-write-test-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
testPath := filepath.Join(tmpDir, "nested", "test.txt")
|
|
content := "hello world"
|
|
|
|
writeT := NewWriteFileTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := writeT.Execute(ctx, map[string]interface{}{
|
|
"path": testPath,
|
|
"content": content,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Errorf("expected success, got error: %s", result.Error)
|
|
}
|
|
|
|
// Verify the file was written
|
|
data, err := os.ReadFile(testPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read written file: %v", err)
|
|
}
|
|
if string(data) != content {
|
|
t.Errorf("expected content %q, got %q", content, string(data))
|
|
}
|
|
}
|
|
|
|
func TestWriteFileToolMissingArgs(t *testing.T) {
|
|
writeT := NewWriteFileTool()
|
|
ctx := context.Background()
|
|
|
|
// Missing path
|
|
result, err := writeT.Execute(ctx, map[string]interface{}{
|
|
"content": "test",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute should not error for invalid args: %v", err)
|
|
}
|
|
if result.Success {
|
|
t.Error("expected failure for missing path")
|
|
}
|
|
|
|
// Missing content
|
|
result, err = writeT.Execute(ctx, map[string]interface{}{
|
|
"path": "/tmp/test.txt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute should not error for invalid args: %v", err)
|
|
}
|
|
if result.Success {
|
|
t.Error("expected failure for missing content")
|
|
}
|
|
}
|
|
|
|
func TestListDirTool(t *testing.T) {
|
|
listT := NewListDirTool()
|
|
|
|
if listT.Name() != "list_dir" {
|
|
t.Errorf("expected name 'list_dir', got %q", listT.Name())
|
|
}
|
|
}
|
|
|
|
func TestListDirToolExecute(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "orca-list-test-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Create some test files
|
|
os.WriteFile(filepath.Join(tmpDir, "a.txt"), []byte("a"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "b.txt"), []byte("bb"), 0644)
|
|
os.Mkdir(filepath.Join(tmpDir, "subdir"), 0755)
|
|
|
|
listT := NewListDirTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := listT.Execute(ctx, map[string]interface{}{
|
|
"path": tmpDir,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Errorf("expected success, got error: %s", result.Error)
|
|
}
|
|
|
|
data := result.Data.(map[string]interface{})
|
|
count := data["count"].(int)
|
|
if count != 3 {
|
|
t.Errorf("expected 3 entries, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestListDirToolRecursive(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "orca-list-rec-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
os.MkdirAll(filepath.Join(tmpDir, "a", "b"), 0755)
|
|
os.WriteFile(filepath.Join(tmpDir, "a", "b", "c.txt"), []byte("c"), 0644)
|
|
|
|
listT := NewListDirTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := listT.Execute(ctx, map[string]interface{}{
|
|
"path": tmpDir,
|
|
"recursive": true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Errorf("expected success, got error: %s", result.Error)
|
|
}
|
|
|
|
data := result.Data.(map[string]interface{})
|
|
entries := data["entries"].([]map[string]interface{})
|
|
if len(entries) < 2 {
|
|
t.Errorf("expected at least 2 recursive entries, got %d", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestListDirToolNonexistent(t *testing.T) {
|
|
listT := NewListDirTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := listT.Execute(ctx, map[string]interface{}{
|
|
"path": "/nonexistent/path",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute should not error for missing path: %v", err)
|
|
}
|
|
if result.Success {
|
|
t.Error("expected failure for nonexistent path")
|
|
}
|
|
}
|
|
|
|
func TestSearchFilesTool(t *testing.T) {
|
|
searchT := NewSearchFilesTool()
|
|
|
|
if searchT.Name() != "search_files" {
|
|
t.Errorf("expected name 'search_files', got %q", searchT.Name())
|
|
}
|
|
}
|
|
|
|
func TestSearchFilesToolExecute(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "orca-search-test-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Create test files
|
|
os.WriteFile(filepath.Join(tmpDir, "findme.go"), []byte("package main\nfunc hello() {\n}\n"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "other.py"), []byte("def world():\n pass\n"), 0644)
|
|
|
|
searchT := NewSearchFilesTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := searchT.Execute(ctx, map[string]interface{}{
|
|
"pattern": "hello",
|
|
"path": tmpDir,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Errorf("expected success, got error: %s", result.Error)
|
|
}
|
|
|
|
data := result.Data.(map[string]interface{})
|
|
count := data["count"].(int)
|
|
if count != 1 {
|
|
t.Errorf("expected 1 match for 'hello', got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSearchFilesToolNoMatch(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "orca-search-nomatch-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("nothing here"), 0644)
|
|
|
|
searchT := NewSearchFilesTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := searchT.Execute(ctx, map[string]interface{}{
|
|
"pattern": "nonexistent-pattern-xyz",
|
|
"path": tmpDir,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Errorf("expected success even with no matches, got error: %s", result.Error)
|
|
}
|
|
|
|
data := result.Data.(map[string]interface{})
|
|
count := data["count"].(int)
|
|
if count != 0 {
|
|
t.Errorf("expected 0 matches, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSearchFilesToolMissingPattern(t *testing.T) {
|
|
searchT := NewSearchFilesTool()
|
|
ctx := context.Background()
|
|
|
|
result, err := searchT.Execute(ctx, map[string]interface{}{})
|
|
if err != nil {
|
|
t.Fatalf("Execute should not error for invalid args: %v", err)
|
|
}
|
|
if result.Success {
|
|
t.Error("expected failure for missing pattern")
|
|
}
|
|
}
|
|
|
|
func TestToolInterfaceSatisfied(t *testing.T) {
|
|
sb := sandbox.NewProcessSandbox()
|
|
|
|
tools := []Tool{
|
|
NewExecTool(sb),
|
|
NewReadFileTool(),
|
|
NewWriteFileTool(),
|
|
NewListDirTool(),
|
|
NewSearchFilesTool(),
|
|
}
|
|
|
|
names := []string{"exec", "read_file", "write_file", "list_dir", "search_files"}
|
|
for i, tool := range tools {
|
|
if tool.Name() != names[i] {
|
|
t.Errorf("expected name %q, got %q", names[i], tool.Name())
|
|
}
|
|
if tool.Description() == "" {
|
|
t.Errorf("tool %q has empty description", names[i])
|
|
}
|
|
if tool.Parameters() == nil {
|
|
t.Errorf("tool %q has nil parameters", names[i])
|
|
}
|
|
}
|
|
}
|