Files
backend/internal/agent/executor_test.go

142 lines
4.1 KiB
Go

package agent
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"code.littlelan.cn/CarrotAssistant/backend/internal/skill"
)
// TestRenderTemplate covers the {{param}} substitution used to build tool
// requests. It must substitute known args, drop unfilled placeholders, and
// leave templates with no markers untouched.
func TestRenderTemplate(t *testing.T) {
args := map[string]any{"keyword": "golang", "limit": 10}
cases := []struct{ in, want string }{
{"https://x.com/search?q={{keyword}}", "https://x.com/search?q=golang"},
{"{{limit}}", "10"},
{"no markers here", "no markers here"},
{"q={{keyword}}&p={{missing}}", "q=golang&p="},
{"{{a}}{{b}}", ""}, // both unfilled -> empty
}
for _, c := range cases {
got := renderTemplate(c.in, args)
if got != c.want {
t.Errorf("renderTemplate(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// TestExecuteReadOnly verifies the default readonly policy blocks mutating
// methods that are neither allow-listed nor confirmation-listed.
func TestExecuteReadOnlyBlocksMutating(t *testing.T) {
tool := skill.Tool{
Name: "delete_post",
Endpoint: skill.Endpoint{Method: "DELETE", URL: "https://example.com/x"},
}
out, err := Execute(context.Background(), tool, skill.Permissions{DefaultMode: "readonly"}, nil)
if err != nil {
t.Fatalf("execute: %v", err)
}
if !out.Denied {
t.Fatalf("expected Denied, got %+v", out)
}
}
// TestExecuteConfirmation verifies a mutating tool on the confirmation list
// pauses execution rather than firing the request.
func TestExecuteConfirmation(t *testing.T) {
called := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(200)
}))
defer srv.Close()
tool := skill.Tool{
Name: "delete_post",
Endpoint: skill.Endpoint{Method: "DELETE", URL: srv.URL + "/x"},
}
perms := skill.Permissions{
DefaultMode: "readonly",
RequireConfirmation: []string{"delete_post"},
}
out, err := Execute(context.Background(), tool, perms, map[string]any{"id": 1})
if err != nil {
t.Fatalf("execute: %v", err)
}
if !out.NeedsConfirmation {
t.Fatalf("expected NeedsConfirmation, got %+v", out)
}
if called {
t.Fatalf("server was called during a confirmation-pending call")
}
}
// TestExecuteAllowWrite verifies a mutating tool on the allow_write list runs
// immediately against the target site.
func TestExecuteAllowWrite(t *testing.T) {
gotMethod := ""
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
w.WriteHeader(200)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
tool := skill.Tool{
Name: "create_post",
Endpoint: skill.Endpoint{
Method: "POST", URL: srv.URL + "/posts",
Body: `{"title":"{{title}}"}`,
},
}
perms := skill.Permissions{
DefaultMode: "readonly",
AllowWrite: []string{"create_post"},
}
out, err := Execute(context.Background(), tool, perms, map[string]any{"title": "hi"})
if err != nil {
t.Fatalf("execute: %v", err)
}
if out.NeedsConfirmation || out.Denied {
t.Fatalf("expected direct execution, got %+v", out)
}
if gotMethod != "POST" {
t.Fatalf("expected POST, got %s", gotMethod)
}
var resp map[string]any
if err := json.Unmarshal([]byte(out.Result), &resp); err != nil || resp["ok"] != true {
t.Fatalf("unexpected result: %s", out.Result)
}
}
// TestExecuteGET verifies a read-only GET runs without any permission entries.
func TestExecuteGET(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("q") != "golang" {
t.Errorf("query q = %q, want golang", r.URL.Query().Get("q"))
}
_, _ = w.Write([]byte(`["post1","post2"]`))
}))
defer srv.Close()
tool := skill.Tool{
Name: "search",
Endpoint: skill.Endpoint{
Method: "GET", URL: srv.URL + "/search",
Query: map[string]string{"q": "{{q}}"},
},
}
out, err := Execute(context.Background(), tool, skill.Permissions{DefaultMode: "readonly"}, map[string]any{"q": "golang"})
if err != nil {
t.Fatalf("execute: %v", err)
}
if out.Result != `["post1","post2"]` {
t.Fatalf("result = %s", out.Result)
}
}