package mcp // 本文件钉住 sdklog.go 的两条分支(线上错误 / 本地错误),sdklog_probe_test.go 则 // 用真 server 证明这条日志面确实走过收敛。 // // 变异判据:把 scrubSDKError 的分支判据摘掉(一律保留原文)⇒ 第一条子用例红; // 反过来「一律只留骨架」⇒ 第二、三条子用例红(运维会丢掉「为什么 session 被关掉」)。 import ( "context" "errors" "fmt" "log/slog" "strings" "testing" sdkjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc" ) // TestScrubSDKError_SplitsWireFromLocal 是分类判据的正面证据。 func TestScrubSDKError_SplitsWireFromLocal(t *testing.T) { t.Parallel() // 1. 线上错误(链上有 *jsonrpc.Error):自由文本一个字节都不留,只留码与长度。 wire := fmt.Errorf("calling %q: %w", "ping", &sdkjsonrpc.Error{Code: sdkjsonrpc.CodeInternalError, Message: "SENTINEL-MCPLOG 她刚才说的话"}) got := scrubSDKError(wire) if strings.Contains(got, "SENTINEL-MCPLOG") { t.Fatalf("线上错误的自由文本进了日志:%q", got) } if !strings.Contains(got, "code=-32603") { t.Fatalf("线上错误的协议码被丢掉了:%q", got) } // 2. 本地错误:原因必须保留(运维靠它判断失败性质)。 if got := scrubSDKError(context.DeadlineExceeded); !strings.Contains(got, "deadline exceeded") { t.Fatalf("本地错误的原因被吞掉了:%q", got) } // 3. 本地错误里的凭据形状仍要打码,换行仍要压平。 leaky := errors.New("dial failed: Authorization: Bearer sk-live-0123456789abcdef\nsecond line") got = scrubSDKError(leaky) if strings.Contains(got, "sk-live") { t.Fatalf("本地错误里的凭据没被打码:%q", got) } if strings.Contains(got, "\n") || !strings.Contains(got, "second line") { t.Fatalf("本地错误的换行没被压平(或压平把内容吞了):%q", got) } } // TestScrubSDKAttr_KeepsScalars 钉住「只收敛自由文本面」:数字/布尔/时间是体积与 // 状态,不该被字符串化或截断。 func TestScrubSDKAttr_KeepsScalars(t *testing.T) { t.Parallel() if got := scrubSDKAttr(slog.Int("consecutiveFailures", 3)); got.Value.Kind().String() != "Int64" { t.Fatalf("数字属性被改成了 %s(应保持数字)", got.Value.Kind()) } }