package kit import ( "encoding/json" "strings" "testing" "time" "unicode/utf8" "github.com/mark3labs/mcp-go/mcp" ) func textOf(t *testing.T, r *mcp.CallToolResult) string { t.Helper() if len(r.Content) == 0 { t.Fatal("结果没有内容") } tc, ok := r.Content[0].(mcp.TextContent) if !ok { t.Fatalf("第一段内容不是文本: %T", r.Content[0]) } return tc.Text } type row struct { SN string `json:"sn"` Phone string `json:"phone"` Note string `json:"note"` Empty *string `json:"empty"` } func TestListLimitAndTruncation(t *testing.T) { rows := make([]row, 300) for i := range rows { rows[i] = row{SN: "1006", Phone: "13800000000", Note: strings.Repeat("x", 50)} } r := List(rows, -1, 100, "") var got ListResult if err := json.Unmarshal([]byte(textOf(t, r)), &got); err != nil { t.Fatal(err) } if got.Total != 300 || got.Returned != 100 || !got.Truncated || got.Hint == "" { t.Errorf("limit 截断不正确: total=%d returned=%d truncated=%v", got.Total, got.Returned, got.Truncated) } item := got.Items[0].(map[string]any) if item["phone"] == "13800000000" { t.Error("phone 应被脱敏") } if _, ok := item["empty"]; ok { t.Error("null 字段应被去掉") } } func TestListCharCap(t *testing.T) { rows := make([]row, 500) for i := range rows { rows[i] = row{SN: "1006", Note: strings.Repeat("长", 200)} } text := textOf(t, List(rows, -1, 500, "")) if utf8.RuneCountInString(text) > MaxResultChars { t.Errorf("结果 %d 字符,超过上限 %d", utf8.RuneCountInString(text), MaxResultChars) } var got ListResult _ = json.Unmarshal([]byte(text), &got) if !got.Truncated || got.Returned >= 500 { t.Errorf("应因字符上限截断: returned=%d", got.Returned) } } func TestListEmpty(t *testing.T) { r := List([]row{}, -1, 10, "") if r.IsError || !strings.Contains(textOf(t, r), "没有") { t.Error("空列表应返回无数据提示") } } func TestTableCharCap(t *testing.T) { cols := []string{"time", "value"} rows := make([][]string, 5000) for i := range rows { rows[i] = []string{"2026-10-01 00:00:00", strings.Repeat("9", 20)} } text := textOf(t, Table(cols, rows, 0, "")) if utf8.RuneCountInString(text) > MaxResultChars { t.Errorf("表格 %d 字符,超过上限", utf8.RuneCountInString(text)) } if !strings.Contains(text, "truncated=true") { t.Error("表格应标注 truncated=true") } } func TestTableMasksByColumn(t *testing.T) { text := textOf(t, Table([]string{"sn", "email"}, [][]string{{"1006", "alice@example.com"}}, 0, "")) if strings.Contains(text, "alice@") { t.Errorf("email 列应脱敏: %s", text) } } func TestExportResource(t *testing.T) { r := Export("fwh://export/test.csv", []string{"a", "b"}, [][]string{{"1", "2"}, {"3", "4"}}, "测试") if len(r.Content) != 2 { t.Fatalf("导出应含文本摘要与资源两段内容,实际 %d", len(r.Content)) } er, ok := r.Content[1].(mcp.EmbeddedResource) if !ok { t.Fatalf("第二段应为 EmbeddedResource: %T", r.Content[1]) } tr := er.Resource.(mcp.TextResourceContents) if tr.MIMEType != "text/csv" || !strings.HasPrefix(tr.Text, "a,b\n1,2\n") { t.Errorf("CSV 内容不正确: %q", tr.Text) } } func TestObjectPrunesAndMasks(t *testing.T) { text := textOf(t, Object(map[string]any{"sn": "1006", "address": "1 Main St", "blank": "", "list": []any{}})) if strings.Contains(text, "Main St") || strings.Contains(text, "blank") || strings.Contains(text, "list") { t.Errorf("对象应脱敏并去掉空值: %s", text) } } func TestParseRange(t *testing.T) { if _, _, err := ParseRange("2026-10-01 00:00:00", "2026-10-01 00:00:00", 0); err == nil { t.Error("from == to 应报错") } if _, _, err := ParseRange("2026-10-01", "2026-12-01", 31*24*time.Hour); err == nil { t.Error("超过最大跨度应报错") } if _, _, err := ParseRange("2026-10-01", "2026-10-02 12:00:00", 31*24*time.Hour); err != nil { t.Errorf("合法范围不应报错: %v", err) } } func TestEnumAndLimit(t *testing.T) { if v, _ := Enum("k", "", "all", "all", "alarm"); v != "all" { t.Error("空值应取默认") } if v, _ := Enum("k", "ALARM", "all", "all", "alarm"); v != "alarm" { t.Error("应大小写不敏感") } if _, err := Enum("k", "x", "all", "all", "alarm"); err == nil { t.Error("非法值应报错") } if Limit(0, 10, 50) != 10 || Limit(99, 10, 50) != 50 || Limit(5, 10, 50) != 5 { t.Error("Limit 归一化不正确") } }