Files
goralphy/agent/tools_test.go
2026-03-04 07:39:05 +01:00

264 lines
7.6 KiB
Go

package agent
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/openai/openai-go"
)
// ─── Hilfsfunktionen ─────────────────────────────────────
func setupWorkDir(t *testing.T) (string, *ToolExecutor) {
t.Helper()
dir, err := os.MkdirTemp("", "agent-tools-test-*")
if err != nil {
t.Fatalf("Konnte temp dir nicht anlegen: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
return dir, NewToolExecutor(dir)
}
func makeToolCall(name, args string) openai.ChatCompletionMessageToolCall {
return openai.ChatCompletionMessageToolCall{
ID: "test-call-id",
Type: "function",
Function: openai.ChatCompletionMessageToolCallFunction{
Name: name,
Arguments: args,
},
}
}
// ─── WRITE_FILE ───────────────────────────────────────────
func TestWriteFile_CreatesFile(t *testing.T) {
dir, ex := setupWorkDir(t)
result, done := ex.Execute(makeToolCall("write_file",
`{"path":"hello.go","content":"package main"}`))
if done {
t.Error("write_file sollte done=false zurückgeben")
}
if !strings.Contains(result, "OK") {
t.Errorf("Erwartete OK im Ergebnis, bekam: %q", result)
}
if _, err := os.Stat(filepath.Join(dir, "hello.go")); os.IsNotExist(err) {
t.Error("Datei hello.go wurde nicht erstellt")
}
}
func TestWriteFile_CorrectContent(t *testing.T) {
dir, ex := setupWorkDir(t)
expected := "package main\n\nfunc main() {}"
ex.Execute(makeToolCall("write_file",
`{"path":"main.go","content":"package main\n\nfunc main() {}"}`))
content, err := os.ReadFile(filepath.Join(dir, "main.go"))
if err != nil {
t.Fatalf("Datei nicht lesbar: %v", err)
}
if string(content) != expected {
t.Errorf("Inhalt falsch\n erwartet: %q\n bekommen: %q", expected, string(content))
}
}
func TestWriteFile_CreatesSubdirectory(t *testing.T) {
dir, ex := setupWorkDir(t)
ex.Execute(makeToolCall("write_file",
`{"path":"subdir/nested/file.go","content":"package sub"}`))
path := filepath.Join(dir, "subdir", "nested", "file.go")
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Error("Verschachtelte Datei wurde nicht angelegt")
}
}
func TestWriteFile_MultilineContent(t *testing.T) {
dir, ex := setupWorkDir(t)
ex.Execute(makeToolCall("write_file",
`{"path":"main.go","content":"package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"http://localhost:8080\")\n}"}`))
content, err := os.ReadFile(filepath.Join(dir, "main.go"))
if err != nil {
t.Fatal("Datei nicht gefunden")
}
if !strings.Contains(string(content), "http://localhost:8080") {
t.Error("Inhalt mit Doppelpunkt/URL wurde falsch geschrieben")
}
}
func TestWriteFile_InvalidJSON_ReturnsError(t *testing.T) {
_, ex := setupWorkDir(t)
result, done := ex.Execute(makeToolCall("write_file", `{invalid json}`))
if done {
t.Error("Sollte done=false zurückgeben")
}
if !strings.Contains(result, "ERROR") {
t.Errorf("Erwartete ERROR, bekam: %q", result)
}
}
// ─── READ_FILE ────────────────────────────────────────────
func TestReadFile_ReadsExistingFile(t *testing.T) {
dir, ex := setupWorkDir(t)
os.WriteFile(filepath.Join(dir, "test.txt"), []byte("Hello World"), 0644)
result, done := ex.Execute(makeToolCall("read_file",
`{"path":"test.txt"}`))
if done {
t.Error("read_file sollte done=false zurückgeben")
}
if !strings.Contains(result, "Hello World") {
t.Errorf("Dateiinhalt fehlt im Ergebnis: %q", result)
}
}
func TestReadFile_NonExistentFile_ReturnsError(t *testing.T) {
_, ex := setupWorkDir(t)
result, _ := ex.Execute(makeToolCall("read_file",
`{"path":"gibts-nicht.txt"}`))
if !strings.Contains(result, "ERROR") {
t.Errorf("Erwartete ERROR, bekam: %q", result)
}
}
func TestReadFile_InvalidJSON_ReturnsError(t *testing.T) {
_, ex := setupWorkDir(t)
result, _ := ex.Execute(makeToolCall("read_file", `{bad}`))
if !strings.Contains(result, "ERROR") {
t.Errorf("Erwartete ERROR, bekam: %q", result)
}
}
// ─── LIST_FILES ───────────────────────────────────────────
func TestListFiles_ShowsFiles(t *testing.T) {
dir, ex := setupWorkDir(t)
os.WriteFile(filepath.Join(dir, "a.go"), []byte(""), 0644)
os.WriteFile(filepath.Join(dir, "b.go"), []byte(""), 0644)
result, _ := ex.Execute(makeToolCall("list_files", `{"path":"."}`))
if !strings.Contains(result, "a.go") || !strings.Contains(result, "b.go") {
t.Errorf("Dateien fehlen im Ergebnis: %q", result)
}
}
func TestListFiles_ShowsTrailingSlashForDirs(t *testing.T) {
dir, ex := setupWorkDir(t)
os.MkdirAll(filepath.Join(dir, "subdir"), 0755)
result, _ := ex.Execute(makeToolCall("list_files", `{"path":"."}`))
if !strings.Contains(result, "subdir/") {
t.Errorf("Verzeichnis ohne Slash: %q", result)
}
}
func TestListFiles_EmptyDir(t *testing.T) {
_, ex := setupWorkDir(t)
result, _ := ex.Execute(makeToolCall("list_files", `{"path":"."}`))
if !strings.Contains(result, "leer") {
t.Errorf("Erwartete 'leer' für leeres Verzeichnis: %q", result)
}
}
func TestListFiles_NonExistentDir_ReturnsError(t *testing.T) {
_, ex := setupWorkDir(t)
result, _ := ex.Execute(makeToolCall("list_files", `{"path":"gibts-nicht"}`))
if !strings.Contains(result, "ERROR") {
t.Errorf("Erwartete ERROR, bekam: %q", result)
}
}
// ─── TASK_COMPLETE ────────────────────────────────────────
func TestTaskComplete_ReturnsDone(t *testing.T) {
_, ex := setupWorkDir(t)
result, done := ex.Execute(makeToolCall("task_complete", `{}`))
if !done {
t.Error("task_complete sollte done=true zurückgeben")
}
if result == "" {
t.Error("task_complete sollte eine Bestätigung zurückgeben")
}
}
// ─── UNBEKANNTES TOOL ─────────────────────────────────────
func TestUnknownTool_ReturnsError(t *testing.T) {
_, ex := setupWorkDir(t)
result, done := ex.Execute(makeToolCall("unknown_tool", `{}`))
if done {
t.Error("Sollte done=false zurückgeben")
}
if !strings.Contains(result, "ERROR") {
t.Errorf("Erwartete ERROR, bekam: %q", result)
}
}
// ─── SICHERHEIT ───────────────────────────────────────────
func TestWriteFile_BlocksPathTraversal(t *testing.T) {
dir, ex := setupWorkDir(t)
result, _ := ex.Execute(makeToolCall("write_file",
`{"path":"../../etc/passwd","content":"hacked"}`))
if !strings.Contains(result, "ERROR") {
t.Errorf("Path Traversal hätte blockiert werden sollen: %q", result)
}
// Sicherstellen dass die Datei nicht angelegt wurde
if _, err := os.Stat(filepath.Join(dir, "../../etc/passwd")); err == nil {
t.Error("Datei außerhalb workDir wurde angelegt!")
}
}
func TestWriteFile_AbsolutePathInsideWorkDir_IsAllowed(t *testing.T) {
dir, ex := setupWorkDir(t)
// Absoluter Pfad der innerhalb des workDir liegt
absPath := filepath.Join(dir, "hello.go")
args := `{"path":"` + absPath + `","content":"package main"}`
result, _ := ex.Execute(makeToolCall("write_file", args))
if strings.Contains(result, "ERROR") {
t.Errorf("Absoluter Pfad innerhalb workDir sollte erlaubt sein: %q", result)
}
}
func TestReadFile_BlocksPathTraversal(t *testing.T) {
_, ex := setupWorkDir(t)
result, _ := ex.Execute(makeToolCall("read_file",
`{"path":"../../etc/passwd"}`))
if !strings.Contains(result, "ERROR") {
t.Errorf("Path Traversal hätte blockiert werden sollen: %q", result)
}
}