Initial commit: CarrotAssistant backend (Go + Gin + SQLite, OpenAI-compatible agent runtime)
This commit is contained in:
141
internal/agent/executor_test.go
Normal file
141
internal/agent/executor_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user