142 lines
4.1 KiB
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)
|
|
}
|
|
}
|