spill_test.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. package helper
  2. import (
  3. "encoding/json"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "github.com/mark3labs/mcp-go/mcp"
  9. )
  10. func TestJSONResultOrFile_SmallInline(t *testing.T) {
  11. res := JSONResultOrFile(map[string]any{"a": 1}, "small")
  12. if res == nil || len(res.Content) == 0 {
  13. t.Fatal("empty result")
  14. }
  15. txt := textOf(t, res)
  16. if strings.Contains(txt, "spilled") {
  17. t.Fatalf("small result should be inline, got: %s", txt)
  18. }
  19. if !strings.Contains(txt, "\"a\": 1") {
  20. t.Fatalf("unexpected inline content: %s", txt)
  21. }
  22. }
  23. func TestJSONResultOrFile_LargeSpill(t *testing.T) {
  24. // 构造超过 InlineMaxBytes 的数据
  25. big := make([]map[string]string, 0, 5000)
  26. for i := 0; i < 5000; i++ {
  27. big = append(big, map[string]string{"k": "value-value-value-value"})
  28. }
  29. res := JSONResultOrFile(big, "large_test")
  30. txt := textOf(t, res)
  31. var meta map[string]any
  32. if err := json.Unmarshal([]byte(txt), &meta); err != nil {
  33. t.Fatalf("expected JSON meta, got: %s", txt)
  34. }
  35. if meta["spilled"] != true {
  36. t.Fatalf("expected spilled=true, got: %v", meta["spilled"])
  37. }
  38. path, _ := meta["localPath"].(string)
  39. if path == "" {
  40. t.Fatal("missing localPath")
  41. }
  42. // 落盘路径必须在 AgentBaseDir 内
  43. if !strings.HasPrefix(path, AgentBaseDir) {
  44. t.Fatalf("spill path outside base dir: %s", path)
  45. }
  46. // 文件应真实存在且内容为完整 JSON
  47. data, err := os.ReadFile(path)
  48. if err != nil {
  49. t.Fatalf("spill file not readable: %v", err)
  50. }
  51. var parsed []map[string]string
  52. if err := json.Unmarshal(data, &parsed); err != nil {
  53. t.Fatalf("spill file not valid JSON: %v", err)
  54. }
  55. if len(parsed) != 5000 {
  56. t.Fatalf("spill file content incomplete: %d", len(parsed))
  57. }
  58. _ = os.Remove(path)
  59. }
  60. func TestValidateSourcePath(t *testing.T) {
  61. okDir := filepath.Join(AgentBaseDir, "downloads")
  62. _ = os.MkdirAll(okDir, 0755)
  63. cases := []struct {
  64. name string
  65. in string
  66. wantErr bool
  67. }{
  68. {"base itself", AgentBaseDir, false},
  69. {"subdir file", filepath.Join(AgentBaseDir, "downloads", "a.json"), false},
  70. {"nested subdir", filepath.Join(AgentBaseDir, "x", "y", "z.txt"), false},
  71. {"traversal escape", filepath.Join(AgentBaseDir, "..", "etc", "passwd"), true},
  72. {"outside abs", "/etc/passwd", true},
  73. {"sibling prefix trick", "/tmp/fwh-toolbox-agent-evil/x", true},
  74. {"empty", "", true},
  75. }
  76. for _, c := range cases {
  77. t.Run(c.name, func(t *testing.T) {
  78. _, err := ValidateSourcePath(c.in)
  79. if c.wantErr && err == nil {
  80. t.Fatalf("expected error for %q", c.in)
  81. }
  82. if !c.wantErr && err != nil {
  83. t.Fatalf("unexpected error for %q: %v", c.in, err)
  84. }
  85. })
  86. }
  87. }
  88. func textOf(t *testing.T, res *mcp.CallToolResult) string {
  89. t.Helper()
  90. if res == nil || len(res.Content) == 0 {
  91. t.Fatal("empty result content")
  92. }
  93. tc, ok := res.Content[0].(mcp.TextContent)
  94. if !ok {
  95. t.Fatalf("content is not text: %T", res.Content[0])
  96. }
  97. return tc.Text
  98. }