188 lines
6.0 KiB
Go
188 lines
6.0 KiB
Go
package io
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
)
|
|
|
|
// fileToolForTest builds a tools set over cfg with opts and returns the file tool.
|
|
func fileToolForTest(t *testing.T, cfg *config.Config, opts ...Option) *fileTool {
|
|
t.Helper()
|
|
return findTool(t, buildTools(t, cfg, opts...), "file").(*fileTool)
|
|
}
|
|
|
|
func TestFileTool_WriteAndRead(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
ft := fileToolForTest(t, &config.Config{DataDir: dir})
|
|
|
|
if _, err := ft.Execute(context.Background(), map[string]any{"action": "write", "path": "sub/a.txt", "content": "hello world"}); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
got, err := ft.Execute(context.Background(), map[string]any{"action": "read", "path": "sub/a.txt"})
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
m := got.(map[string]any)
|
|
if m["content"] != "hello world" {
|
|
t.Fatalf("content = %q", m["content"])
|
|
}
|
|
if m["size"] != len("hello world") {
|
|
t.Fatalf("size = %v", m["size"])
|
|
}
|
|
if m["path"] != filepath.Join(dir, "sub", "a.txt") {
|
|
t.Fatalf("path = %q", m["path"])
|
|
}
|
|
}
|
|
|
|
func TestFileTool_RejectsTraversal(t *testing.T) {
|
|
t.Parallel()
|
|
ft := fileToolForTest(t, &config.Config{DataDir: t.TempDir()})
|
|
cases := []string{
|
|
"../escape.txt",
|
|
"../../../../etc/passwd",
|
|
"sub/../../escape.txt",
|
|
}
|
|
for _, p := range cases {
|
|
_, err := ft.Execute(context.Background(), map[string]any{"action": "read", "path": p})
|
|
if err == nil {
|
|
t.Fatalf("expected traversal rejection for %q", p)
|
|
}
|
|
}
|
|
// An absolute path outside the data dir is also rejected.
|
|
outside := filepath.Join(t.TempDir(), "outside.txt")
|
|
_, err := ft.Execute(context.Background(), map[string]any{"action": "write", "path": outside, "content": "x"})
|
|
if err == nil {
|
|
t.Fatal("expected rejection for absolute path outside the data dir")
|
|
}
|
|
}
|
|
|
|
func TestFileTool_ReadSizeLimit(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
// Write with the default limit, then read with a tiny cap.
|
|
if _, err := fileToolForTest(t, &config.Config{DataDir: dir}).
|
|
Execute(context.Background(), map[string]any{"action": "write", "path": "big.txt", "content": "1234567890"}); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
ft := fileToolForTest(t, &config.Config{DataDir: dir}, WithReadLimit(4))
|
|
_, err := ft.Execute(context.Background(), map[string]any{"action": "read", "path": "big.txt"})
|
|
if err == nil || !strings.Contains(err.Error(), "read limit") {
|
|
t.Fatalf("expected read-limit error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFileTool_WriteSizeLimit(t *testing.T) {
|
|
t.Parallel()
|
|
ft := fileToolForTest(t, &config.Config{DataDir: t.TempDir()}, WithWriteLimit(4))
|
|
_, err := ft.Execute(context.Background(), map[string]any{"action": "write", "path": "x.txt", "content": "too long"})
|
|
if err == nil || !strings.Contains(err.Error(), "write limit") {
|
|
t.Fatalf("expected write-limit error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFileTool_List(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
ft := fileToolForTest(t, &config.Config{DataDir: dir})
|
|
for _, name := range []string{"a.txt", "b.txt", "sub"} {
|
|
p := filepath.Join(dir, name)
|
|
if name == "sub" {
|
|
if err := os.MkdirAll(p, dirPerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
} else if err := os.WriteFile(p, []byte("x"), filePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
got, err := ft.Execute(context.Background(), map[string]any{"action": "list", "path": "."})
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
m := got.(map[string]any)
|
|
if m["count"].(int) != 3 {
|
|
t.Fatalf("count = %v", m["count"])
|
|
}
|
|
entries := m["entries"].([]map[string]any)
|
|
found := map[string]bool{}
|
|
for _, e := range entries {
|
|
found[e["name"].(string)] = true
|
|
}
|
|
for _, want := range []string{"a.txt", "b.txt", "sub"} {
|
|
if !found[want] {
|
|
t.Errorf("missing entry %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFileTool_Search(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
ft := fileToolForTest(t, &config.Config{DataDir: dir})
|
|
for _, name := range []string{"report_2026.txt", "notes.md", "report_old.txt"} {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), filePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
got, err := ft.Execute(context.Background(), map[string]any{"action": "search", "query": "report"})
|
|
if err != nil {
|
|
t.Fatalf("search: %v", err)
|
|
}
|
|
m := got.(map[string]any)
|
|
if m["count"].(int) != 2 {
|
|
t.Fatalf("count = %v", m["count"])
|
|
}
|
|
if _, err := ft.Execute(context.Background(), map[string]any{"action": "search", "query": ""}); err == nil {
|
|
t.Fatal("expected error for empty search query")
|
|
}
|
|
}
|
|
|
|
func TestFileTool_NoBaseDirectory(t *testing.T) {
|
|
t.Parallel()
|
|
ft := fileToolForTest(t, &config.Config{}) // empty DataDir, no whitelist
|
|
_, err := ft.Execute(context.Background(), map[string]any{"action": "read", "path": "a.txt"})
|
|
if err == nil || !strings.Contains(err.Error(), "no base directory") {
|
|
t.Fatalf("expected no-base-directory error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFileTool_PathWhitelist(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
extra := t.TempDir()
|
|
ft := fileToolForTest(t, &config.Config{DataDir: dir}, WithFilePathWhitelist(extra))
|
|
if err := os.WriteFile(filepath.Join(extra, "w.txt"), []byte("hi"), filePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := ft.Execute(context.Background(), map[string]any{"action": "read", "path": filepath.Join(extra, "w.txt")})
|
|
if err != nil {
|
|
t.Fatalf("read whitelisted file: %v", err)
|
|
}
|
|
if got.(map[string]any)["content"] != "hi" {
|
|
t.Fatalf("content = %q", got.(map[string]any)["content"])
|
|
}
|
|
}
|
|
|
|
func TestFileTool_RejectsSymlinkEscape(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
secret := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("hidden"), filePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
link := filepath.Join(dir, "link.txt")
|
|
if err := os.Symlink(filepath.Join(secret, "secret.txt"), link); err != nil {
|
|
t.Skipf("symlink unsupported: %v", err)
|
|
}
|
|
ft := fileToolForTest(t, &config.Config{DataDir: dir})
|
|
_, err := ft.Execute(context.Background(), map[string]any{"action": "read", "path": "link.txt"})
|
|
if err == nil {
|
|
t.Fatal("expected symlink escape to be rejected")
|
|
}
|
|
}
|