Umstellung auf Model Tools
This commit is contained in:
@@ -3,48 +3,64 @@ package agent
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/openai/openai-go"
|
||||
)
|
||||
|
||||
// Hilfsfunktion: temporäres Arbeitsverzeichnis anlegen
|
||||
func setupWorkDir(t *testing.T) string {
|
||||
// ─── 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) }) // wird nach jedem Test aufgeräumt
|
||||
return dir
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
return dir, NewToolExecutor(dir)
|
||||
}
|
||||
|
||||
// ─── WRITE_FILE ──────────────────────────────────────────
|
||||
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 := setupWorkDir(t)
|
||||
dir, ex := setupWorkDir(t)
|
||||
|
||||
toolCall := "TOOL:WRITE_FILE:hello.go:package main\n\nfunc main() {}"
|
||||
output := executeTool(toolCall, dir)
|
||||
result, done := ex.Execute(makeToolCall("write_file",
|
||||
`{"path":"hello.go","content":"package main"}`))
|
||||
|
||||
if output != "WRITE_FILE OK: hello.go geschrieben" {
|
||||
t.Errorf("Unerwarteter Output: %q", output)
|
||||
if done {
|
||||
t.Error("write_file sollte done=false zurückgeben")
|
||||
}
|
||||
|
||||
// Datei muss wirklich existieren
|
||||
path := filepath.Join(dir, "hello.go")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
t.Error("Datei wurde nicht angelegt")
|
||||
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 := setupWorkDir(t)
|
||||
dir, ex := setupWorkDir(t)
|
||||
expected := "package main\n\nfunc main() {}"
|
||||
|
||||
executeTool("TOOL:WRITE_FILE:hello.go:"+expected, dir)
|
||||
ex.Execute(makeToolCall("write_file",
|
||||
`{"path":"main.go","content":"package main\n\nfunc main() {}"}`))
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(dir, "hello.go"))
|
||||
content, err := os.ReadFile(filepath.Join(dir, "main.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("Datei konnte nicht gelesen werden: %v", err)
|
||||
t.Fatalf("Datei nicht lesbar: %v", err)
|
||||
}
|
||||
if string(content) != expected {
|
||||
t.Errorf("Inhalt falsch\n erwartet: %q\n bekommen: %q", expected, string(content))
|
||||
@@ -52,9 +68,10 @@ func TestWriteFile_CorrectContent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWriteFile_CreatesSubdirectory(t *testing.T) {
|
||||
dir := setupWorkDir(t)
|
||||
dir, ex := setupWorkDir(t)
|
||||
|
||||
executeTool("TOOL:WRITE_FILE:subdir/nested/file.go:package sub", dir)
|
||||
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) {
|
||||
@@ -62,135 +79,185 @@ func TestWriteFile_CreatesSubdirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFile_MissingContent_ReturnsError(t *testing.T) {
|
||||
dir := setupWorkDir(t)
|
||||
func TestWriteFile_MultilineContent(t *testing.T) {
|
||||
dir, ex := setupWorkDir(t)
|
||||
|
||||
output := executeTool("TOOL:WRITE_FILE:hello.go", dir)
|
||||
ex.Execute(makeToolCall("write_file",
|
||||
`{"path":"main.go","content":"package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"http://localhost:8080\")\n}"}`))
|
||||
|
||||
if output != "ERROR: WRITE_FILE braucht Inhalt" {
|
||||
t.Errorf("Erwartete Fehlermeldung, bekam: %q", output)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── READ_FILE ───────────────────────────────────────────
|
||||
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 := setupWorkDir(t)
|
||||
expected := "Hello, World!"
|
||||
dir, ex := setupWorkDir(t)
|
||||
os.WriteFile(filepath.Join(dir, "test.txt"), []byte("Hello World"), 0644)
|
||||
|
||||
// Datei vorbereiten
|
||||
os.WriteFile(filepath.Join(dir, "test.txt"), []byte(expected), 0644)
|
||||
result, done := ex.Execute(makeToolCall("read_file",
|
||||
`{"path":"test.txt"}`))
|
||||
|
||||
output := executeTool("TOOL:READ_FILE:test.txt", dir)
|
||||
|
||||
if output != "READ_FILE test.txt:\n"+expected {
|
||||
t.Errorf("Unerwarteter Output: %q", output)
|
||||
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) {
|
||||
dir := setupWorkDir(t)
|
||||
_, ex := setupWorkDir(t)
|
||||
|
||||
output := executeTool("TOOL:READ_FILE:gibts-nicht.txt", dir)
|
||||
result, _ := ex.Execute(makeToolCall("read_file",
|
||||
`{"path":"gibts-nicht.txt"}`))
|
||||
|
||||
if output == "" || output[:15] != "READ_FILE ERROR" {
|
||||
t.Errorf("Erwartete Fehlermeldung, bekam: %q", output)
|
||||
if !strings.Contains(result, "ERROR") {
|
||||
t.Errorf("Erwartete ERROR, bekam: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── LIST_FILES ──────────────────────────────────────────
|
||||
func TestReadFile_InvalidJSON_ReturnsError(t *testing.T) {
|
||||
_, ex := setupWorkDir(t)
|
||||
|
||||
func TestListFiles_ReturnsFileNames(t *testing.T) {
|
||||
dir := setupWorkDir(t)
|
||||
result, _ := ex.Execute(makeToolCall("read_file", `{bad}`))
|
||||
|
||||
// Testdateien anlegen
|
||||
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)
|
||||
|
||||
output := executeTool("TOOL:LIST_FILES:.", dir)
|
||||
result, _ := ex.Execute(makeToolCall("list_files", `{"path":"."}`))
|
||||
|
||||
if !contains(output, "a.go") || !contains(output, "b.go") {
|
||||
t.Errorf("Erwartete beide Dateien in Output: %q", output)
|
||||
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) {
|
||||
dir := setupWorkDir(t)
|
||||
_, ex := setupWorkDir(t)
|
||||
|
||||
output := executeTool("TOOL:LIST_FILES:gibts-nicht", dir)
|
||||
result, _ := ex.Execute(makeToolCall("list_files", `{"path":"gibts-nicht"}`))
|
||||
|
||||
if output[:16] != "LIST_FILES ERROR" {
|
||||
t.Errorf("Erwartete Fehlermeldung, bekam: %q", output)
|
||||
if !strings.Contains(result, "ERROR") {
|
||||
t.Errorf("Erwartete ERROR, bekam: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── UNGÜLTIGE TOOL-CALLS ────────────────────────────────
|
||||
// ─── TASK_COMPLETE ────────────────────────────────────────
|
||||
|
||||
func TestExecuteTool_UnknownTool_ReturnsError(t *testing.T) {
|
||||
dir := setupWorkDir(t)
|
||||
func TestTaskComplete_ReturnsDone(t *testing.T) {
|
||||
_, ex := setupWorkDir(t)
|
||||
|
||||
output := executeTool("TOOL:UNKNOWN_TOOL:arg", dir)
|
||||
result, done := ex.Execute(makeToolCall("task_complete", `{}`))
|
||||
|
||||
if output[:5] != "ERROR" {
|
||||
t.Errorf("Erwartete Fehlermeldung, bekam: %q", output)
|
||||
if !done {
|
||||
t.Error("task_complete sollte done=true zurückgeben")
|
||||
}
|
||||
if result == "" {
|
||||
t.Error("task_complete sollte eine Bestätigung zurückgeben")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteTool_InvalidFormat_ReturnsError(t *testing.T) {
|
||||
dir := setupWorkDir(t)
|
||||
// ─── UNBEKANNTES TOOL ─────────────────────────────────────
|
||||
|
||||
output := executeTool("TOOL:", dir)
|
||||
func TestUnknownTool_ReturnsError(t *testing.T) {
|
||||
_, ex := setupWorkDir(t)
|
||||
|
||||
if output[:5] != "ERROR" {
|
||||
t.Errorf("Erwartete Fehlermeldung, bekam: %q", output)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── EXECUTE_TOOLS (Top-Level) ───────────────────────────
|
||||
// ─── SICHERHEIT ───────────────────────────────────────────
|
||||
|
||||
func TestExecuteTools_ParsesMultipleToolCalls(t *testing.T) {
|
||||
dir := setupWorkDir(t)
|
||||
func TestWriteFile_BlocksPathTraversal(t *testing.T) {
|
||||
dir, ex := setupWorkDir(t)
|
||||
|
||||
response := `Ich schreibe zwei Dateien:
|
||||
TOOL:WRITE_FILE:foo.go:package foo
|
||||
TOOL:WRITE_FILE:bar.go:package bar`
|
||||
result, _ := ex.Execute(makeToolCall("write_file",
|
||||
`{"path":"../../etc/passwd","content":"hacked"}`))
|
||||
|
||||
_, hadTools := ExecuteTools(response, dir)
|
||||
|
||||
if !hadTools {
|
||||
t.Error("Hätte Tool-Calls erkennen sollen")
|
||||
if !strings.Contains(result, "ERROR") {
|
||||
t.Errorf("Path Traversal hätte blockiert werden sollen: %q", result)
|
||||
}
|
||||
|
||||
// Beide Dateien müssen existieren
|
||||
for _, name := range []string{"foo.go", "bar.go"} {
|
||||
if _, err := os.Stat(filepath.Join(dir, name)); os.IsNotExist(err) {
|
||||
t.Errorf("Datei %s wurde nicht angelegt", name)
|
||||
}
|
||||
// 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 TestExecuteTools_NoToolCalls_ReturnsFalse(t *testing.T) {
|
||||
dir := setupWorkDir(t)
|
||||
func TestWriteFile_AbsolutePathInsideWorkDir_IsAllowed(t *testing.T) {
|
||||
dir, ex := setupWorkDir(t)
|
||||
|
||||
_, hadTools := ExecuteTools("Keine Tools hier, nur Text.", dir)
|
||||
// Absoluter Pfad der innerhalb des workDir liegt
|
||||
absPath := filepath.Join(dir, "hello.go")
|
||||
args := `{"path":"` + absPath + `","content":"package main"}`
|
||||
|
||||
if hadTools {
|
||||
t.Error("Hätte keine Tool-Calls erkennen sollen")
|
||||
result, _ := ex.Execute(makeToolCall("write_file", args))
|
||||
|
||||
if strings.Contains(result, "ERROR") {
|
||||
t.Errorf("Absoluter Pfad innerhalb workDir sollte erlaubt sein: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Hilfsfunktion ───────────────────────────────────────
|
||||
func TestReadFile_BlocksPathTraversal(t *testing.T) {
|
||||
_, ex := setupWorkDir(t)
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr ||
|
||||
len(s) > 0 && containsHelper(s, substr))
|
||||
}
|
||||
result, _ := ex.Execute(makeToolCall("read_file",
|
||||
`{"path":"../../etc/passwd"}`))
|
||||
|
||||
func containsHelper(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
if !strings.Contains(result, "ERROR") {
|
||||
t.Errorf("Path Traversal hätte blockiert werden sollen: %q", result)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user