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, "

Hello World

"+strings.Repeat("y", 500)+"") })) 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() } }