264 lines
7.6 KiB
Go
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)
|
|
}
|
|
}
|