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) } }