a431d1ec95
Add OpenAI-compatible providers, TUI, slash commands, and model switching. Add local file tools plus optional yolo shell execution. Document config, usage, and development workflow.
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFileTools(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
write := NewFileWriteTool(dir)
|
|
if out, err := write.Execute(ctx, json.RawMessage(`{"path":"notes/a.txt","content":"hello world"}`)); err != nil {
|
|
t.Fatal(err)
|
|
} else if !strings.Contains(out, "wrote 11 bytes") {
|
|
t.Fatalf("write output = %q", out)
|
|
}
|
|
|
|
read := NewFileReadTool(dir)
|
|
if out, err := read.Execute(ctx, json.RawMessage(`{"path":"notes/a.txt"}`)); err != nil {
|
|
t.Fatal(err)
|
|
} else if out != "hello world" {
|
|
t.Fatalf("read output = %q", out)
|
|
}
|
|
|
|
list := NewFileListTool(dir)
|
|
if out, err := list.Execute(ctx, json.RawMessage(`{"path":"notes"}`)); err != nil {
|
|
t.Fatal(err)
|
|
} else if out != "a.txt" {
|
|
t.Fatalf("list output = %q", out)
|
|
}
|
|
|
|
search := NewFileSearchTool(dir)
|
|
if out, err := search.Execute(ctx, json.RawMessage(`{"query":"hello","path":"notes"}`)); err != nil {
|
|
t.Fatal(err)
|
|
} else if !strings.Contains(out, "hello world") {
|
|
t.Fatalf("search output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestShellRunTool(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "x.txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tool := NewShellRunTool(dir)
|
|
out, err := tool.Execute(context.Background(), json.RawMessage(`{"command":"pwd && ls"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, dir) || !strings.Contains(out, "x.txt") {
|
|
t.Fatalf("output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestReadOnlyBuiltinsExposeOpenAICompatibleNamesAndAliases(t *testing.T) {
|
|
registry := ReadOnlyBuiltins(t.TempDir())
|
|
var names []string
|
|
for _, def := range registry.Definitions() {
|
|
names = append(names, def.Function.Name)
|
|
if strings.Contains(def.Function.Name, ".") {
|
|
t.Fatalf("tool name contains dot: %s", def.Function.Name)
|
|
}
|
|
}
|
|
|
|
for _, want := range []string{"file_list", "file_read", "file_search"} {
|
|
if !slices.Contains(names, want) {
|
|
t.Fatalf("definitions missing %s: %#v", want, names)
|
|
}
|
|
}
|
|
for _, alias := range []string{"file.list", "file.read", "file.search"} {
|
|
if _, ok := registry.Get(alias); !ok {
|
|
t.Fatalf("alias missing: %s", alias)
|
|
}
|
|
}
|
|
if _, ok := registry.Get("file_write"); ok {
|
|
t.Fatal("read-only registry should not expose file_write")
|
|
}
|
|
}
|
|
|
|
func TestBuiltinsExposeYoloTools(t *testing.T) {
|
|
registry := Builtins(t.TempDir())
|
|
for _, name := range []string{"file_write", "shell_run", "file.write", "shell.run"} {
|
|
if _, ok := registry.Get(name); !ok {
|
|
t.Fatalf("tool missing: %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAgentInstructionsExplainCommandModes(t *testing.T) {
|
|
readOnly := AgentInstructions("/tmp/project", false)
|
|
for _, want := range []string{"Shell command execution is disabled", "--yolo", "Do not call tools for greetings", "Do not explore the project preemptively", "Do not call file_list just to discover context"} {
|
|
if !strings.Contains(readOnly, want) {
|
|
t.Fatalf("read-only instructions missing %q:\n%s", want, readOnly)
|
|
}
|
|
}
|
|
|
|
yolo := AgentInstructions("/tmp/project", true)
|
|
for _, want := range []string{"use shell_run", "commands written in backticks", "Do not start interactive", "non-interactive local commands", "Do not call tools for greetings"} {
|
|
if !strings.Contains(yolo, want) {
|
|
t.Fatalf("yolo instructions missing %q:\n%s", want, yolo)
|
|
}
|
|
}
|
|
}
|