result_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package kit
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "time"
  7. "unicode/utf8"
  8. "github.com/mark3labs/mcp-go/mcp"
  9. )
  10. func textOf(t *testing.T, r *mcp.CallToolResult) string {
  11. t.Helper()
  12. if len(r.Content) == 0 {
  13. t.Fatal("结果没有内容")
  14. }
  15. tc, ok := r.Content[0].(mcp.TextContent)
  16. if !ok {
  17. t.Fatalf("第一段内容不是文本: %T", r.Content[0])
  18. }
  19. return tc.Text
  20. }
  21. type row struct {
  22. SN string `json:"sn"`
  23. Phone string `json:"phone"`
  24. Note string `json:"note"`
  25. Empty *string `json:"empty"`
  26. }
  27. func TestListLimitAndTruncation(t *testing.T) {
  28. rows := make([]row, 300)
  29. for i := range rows {
  30. rows[i] = row{SN: "1006", Phone: "13800000000", Note: strings.Repeat("x", 50)}
  31. }
  32. r := List(rows, -1, 100, "")
  33. var got ListResult
  34. if err := json.Unmarshal([]byte(textOf(t, r)), &got); err != nil {
  35. t.Fatal(err)
  36. }
  37. if got.Total != 300 || got.Returned != 100 || !got.Truncated || got.Hint == "" {
  38. t.Errorf("limit 截断不正确: total=%d returned=%d truncated=%v", got.Total, got.Returned, got.Truncated)
  39. }
  40. item := got.Items[0].(map[string]any)
  41. if item["phone"] == "13800000000" {
  42. t.Error("phone 应被脱敏")
  43. }
  44. if _, ok := item["empty"]; ok {
  45. t.Error("null 字段应被去掉")
  46. }
  47. }
  48. func TestListCharCap(t *testing.T) {
  49. rows := make([]row, 500)
  50. for i := range rows {
  51. rows[i] = row{SN: "1006", Note: strings.Repeat("长", 200)}
  52. }
  53. text := textOf(t, List(rows, -1, 500, ""))
  54. if utf8.RuneCountInString(text) > MaxResultChars {
  55. t.Errorf("结果 %d 字符,超过上限 %d", utf8.RuneCountInString(text), MaxResultChars)
  56. }
  57. var got ListResult
  58. _ = json.Unmarshal([]byte(text), &got)
  59. if !got.Truncated || got.Returned >= 500 {
  60. t.Errorf("应因字符上限截断: returned=%d", got.Returned)
  61. }
  62. }
  63. func TestListEmpty(t *testing.T) {
  64. r := List([]row{}, -1, 10, "")
  65. if r.IsError || !strings.Contains(textOf(t, r), "没有") {
  66. t.Error("空列表应返回无数据提示")
  67. }
  68. }
  69. func TestTableCharCap(t *testing.T) {
  70. cols := []string{"time", "value"}
  71. rows := make([][]string, 5000)
  72. for i := range rows {
  73. rows[i] = []string{"2026-10-01 00:00:00", strings.Repeat("9", 20)}
  74. }
  75. text := textOf(t, Table(cols, rows, 0, ""))
  76. if utf8.RuneCountInString(text) > MaxResultChars {
  77. t.Errorf("表格 %d 字符,超过上限", utf8.RuneCountInString(text))
  78. }
  79. if !strings.Contains(text, "truncated=true") {
  80. t.Error("表格应标注 truncated=true")
  81. }
  82. }
  83. func TestTableMasksByColumn(t *testing.T) {
  84. text := textOf(t, Table([]string{"sn", "email"}, [][]string{{"1006", "alice@example.com"}}, 0, ""))
  85. if strings.Contains(text, "alice@") {
  86. t.Errorf("email 列应脱敏: %s", text)
  87. }
  88. }
  89. func TestExportResource(t *testing.T) {
  90. r := Export("fwh://export/test.csv", []string{"a", "b"}, [][]string{{"1", "2"}, {"3", "4"}}, "测试")
  91. if len(r.Content) != 2 {
  92. t.Fatalf("导出应含文本摘要与资源两段内容,实际 %d", len(r.Content))
  93. }
  94. er, ok := r.Content[1].(mcp.EmbeddedResource)
  95. if !ok {
  96. t.Fatalf("第二段应为 EmbeddedResource: %T", r.Content[1])
  97. }
  98. tr := er.Resource.(mcp.TextResourceContents)
  99. if tr.MIMEType != "text/csv" || !strings.HasPrefix(tr.Text, "a,b\n1,2\n") {
  100. t.Errorf("CSV 内容不正确: %q", tr.Text)
  101. }
  102. }
  103. func TestObjectPrunesAndMasks(t *testing.T) {
  104. text := textOf(t, Object(map[string]any{"sn": "1006", "address": "1 Main St", "blank": "", "list": []any{}}))
  105. if strings.Contains(text, "Main St") || strings.Contains(text, "blank") || strings.Contains(text, "list") {
  106. t.Errorf("对象应脱敏并去掉空值: %s", text)
  107. }
  108. }
  109. func TestParseRange(t *testing.T) {
  110. if _, _, err := ParseRange("2026-10-01 00:00:00", "2026-10-01 00:00:00", 0); err == nil {
  111. t.Error("from == to 应报错")
  112. }
  113. if _, _, err := ParseRange("2026-10-01", "2026-12-01", 31*24*time.Hour); err == nil {
  114. t.Error("超过最大跨度应报错")
  115. }
  116. if _, _, err := ParseRange("2026-10-01", "2026-10-02 12:00:00", 31*24*time.Hour); err != nil {
  117. t.Errorf("合法范围不应报错: %v", err)
  118. }
  119. }
  120. func TestEnumAndLimit(t *testing.T) {
  121. if v, _ := Enum("k", "", "all", "all", "alarm"); v != "all" {
  122. t.Error("空值应取默认")
  123. }
  124. if v, _ := Enum("k", "ALARM", "all", "all", "alarm"); v != "alarm" {
  125. t.Error("应大小写不敏感")
  126. }
  127. if _, err := Enum("k", "x", "all", "all", "alarm"); err == nil {
  128. t.Error("非法值应报错")
  129. }
  130. if Limit(0, 10, 50) != 10 || Limit(99, 10, 50) != 50 || Limit(5, 10, 50) != 5 {
  131. t.Error("Limit 归一化不正确")
  132. }
  133. }