feat: config, OpenAI-compatible streaming provider and safe tools
This commit is contained in:
commit
c1c7b0e005
13 changed files with 1031 additions and 0 deletions
92
internal/tools/tools_test.go
Normal file
92
internal/tools/tools_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTimeNow(t *testing.T) {
|
||||
fixed := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
out, err := TimeNow{Now: func() time.Time { return fixed }}.Call(context.Background(), json.RawMessage(`{"timezone":"Europe/Berlin"}`))
|
||||
if err != nil || !strings.HasPrefix(out, "2026-01-02T04:04:05") {
|
||||
t.Fatalf("%q %v", out, err)
|
||||
}
|
||||
if _, err := (TimeNow{}).Call(context.Background(), json.RawMessage(`{"timezone":"Nope/Nope"}`)); err == nil {
|
||||
t.Fatal("expected tz error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkspaceConfinement(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ws := Workspace{Dir: filepath.Join(dir, "ws"), MaxBytes: 100}
|
||||
ctx := context.Background()
|
||||
w, r, l := FileWrite{ws}, FileRead{ws}, FileList{ws}
|
||||
if _, err := w.Call(ctx, json.RawMessage(`{"path":"a/b.txt","content":"hi"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out, err := r.Call(ctx, json.RawMessage(`{"path":"a/b.txt"}`)); err != nil || out != "hi" {
|
||||
t.Fatalf("%q %v", out, err)
|
||||
}
|
||||
if out, _ := l.Call(ctx, json.RawMessage(`{}`)); out != "a/\n" {
|
||||
t.Fatalf("list %q", out)
|
||||
}
|
||||
os.WriteFile(filepath.Join(dir, "secret"), []byte("x"), 0o600)
|
||||
os.Symlink(filepath.Join(dir, "secret"), filepath.Join(ws.Dir, "link"))
|
||||
for _, p := range []string{"../secret", "/etc/passwd", "a/../../secret", "link"} {
|
||||
if _, err := r.Call(ctx, json.RawMessage(fmt.Sprintf(`{"path":%q}`, p))); err == nil {
|
||||
t.Errorf("read of %q should fail", p)
|
||||
}
|
||||
if _, err := w.Call(ctx, json.RawMessage(fmt.Sprintf(`{"path":%q,"content":"x"}`, p))); err == nil {
|
||||
t.Errorf("write of %q should fail", p)
|
||||
}
|
||||
}
|
||||
if _, err := w.Call(ctx, json.RawMessage(`{"path":"big","content":"`+strings.Repeat("x", 101)+`"}`)); err == nil {
|
||||
t.Error("size limit not enforced")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebFetchBlocksPrivateByDefault(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "ok") }))
|
||||
defer srv.Close()
|
||||
_, err := NewWebFetch(2*time.Second, 1024, false).Call(context.Background(), json.RawMessage(`{"url":"`+srv.URL+`"}`))
|
||||
if err == nil || !errors.Is(err, ErrBlockedAddr) {
|
||||
t.Fatalf("expected blocked, got %v", err)
|
||||
}
|
||||
if _, err := NewWebFetch(time.Second, 10, false).Call(context.Background(), json.RawMessage(`{"url":"file:///etc/passwd"}`)); err == nil {
|
||||
t.Fatal("file scheme must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebFetchStripsAndTruncates(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, "<html><script>evil()</script><p>Hello <b>World</b></p>"+strings.Repeat("y", 500)+"</html>")
|
||||
}))
|
||||
defer srv.Close()
|
||||
out, err := NewWebFetch(2*time.Second, 80, true).Call(context.Background(), json.RawMessage(`{"url":"`+srv.URL+`"}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(out, "evil") || !strings.Contains(out, "Hello World") || !strings.HasSuffix(out, "[truncated]") {
|
||||
t.Fatalf("unexpected: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryUnknownAndBadJSON(t *testing.T) {
|
||||
r := NewRegistry(TimeNow{})
|
||||
if !strings.Contains(r.Call(context.Background(), "nope", "{}"), "unknown tool") {
|
||||
t.Fatal()
|
||||
}
|
||||
if !strings.Contains(r.Call(context.Background(), "time_now", "{bad"), "not valid JSON") {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue