package trace // 本文件是 M49 的核心断言:**守卫在构造函数里**。 // // M46 把令牌守卫加在调用方(model / voice 各一份),M48 复审的中危 finding 证明那是 // 漂移形态 —— 构造函数自己不守,任何新调用点默认就不安全,四条出口(httpclient / // vision / embedding / weixin)恰恰一直漏着。这里把断言钉在 UpstreamError 与 // SetCode / SetType 上:摘掉构造函数里的守卫,本文件与四条出口的用例同时变红。 // // 每条都是「把它放宽就变红」的形状:断言的是真正进日志的两个出口(Error() / Attrs())。 import ( "bytes" "errors" "log/slog" "strings" "testing" ) // TestUpstreamError_ConstructorDropsFreeTextCodeAndType 报文里 error.code 与 // error.type 都是回显的散文 ⇒ 两个字段在构造时就变空串,而 status / body_bytes / // body_sha256 仍在(诊断面不能一起丢)。 func TestUpstreamError_ConstructorDropsFreeTextCodeAndType(t *testing.T) { t.Parallel() const prose = "Invalid value: '她刚才说的那句话'" body := []byte(`{"error":{"code":"` + prose + `","type":"` + prose + `"}}`) err := UpstreamError("api.example.com", 400, "application/json", body) info, ok := err.(*UpstreamErrorInfo) if !ok { t.Fatalf("err 类型 = %T, want *UpstreamErrorInfo", err) } if info.Code != "" || info.Type != "" { t.Fatalf("info.Code/Type = %q/%q,want 空串(构造即安全)", info.Code, info.Type) } var logBuf bytes.Buffer slog.New(slog.NewJSONHandler(&logBuf, nil)).Error("upstream call failed", "err", err) for _, surface := range []string{err.Error(), logBuf.String(), strings.Join(anyToStrings(info.Attrs()), " ")} { if strings.Contains(surface, "她刚才说的那句话") { t.Fatalf("散文经 code/type 进了出口:%s", surface) } if strings.Contains(surface, " code=") || strings.Contains(surface, " type=") { t.Fatalf("非令牌形状的 code/type 没有被丢弃:%s", surface) } } for _, want := range []string{"status=400", "body_bytes=", "body_sha256=", "api.example.com"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error = %q,want 含 %q(诊断线索不能一起丢)", err.Error(), want) } } if !errors.Is(err, ErrUpstream) { t.Fatal("errors.Is(err, ErrUpstream) = false;降级链无法按类别判断") } } // TestUpstreamError_ConstructorKeepsProtocolTokenCodeAndType 真实的协议枚举必须原样 // 通过:丢了它们只是少一条分类提示,但那是**有价值的**提示,别顺手一起丢。 func TestUpstreamError_ConstructorKeepsProtocolTokenCodeAndType(t *testing.T) { t.Parallel() body := []byte(`{"error":{"code":"invalid_request_error","type":"Throttling.RateQuota"}}`) err := UpstreamError("api.example.com", 429, "application/json", body) info := err.(*UpstreamErrorInfo) if info.Code != "invalid_request_error" || info.Type != "Throttling.RateQuota" { t.Fatalf("info.Code/Type = %q/%q,want 原值(令牌形状必须保留)", info.Code, info.Type) } for _, want := range []string{"code=invalid_request_error", "type=Throttling.RateQuota"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error = %q,want 含 %q", err.Error(), want) } } } // TestUpstreamError_SetCodeGuardsPostConstructionInjection 构造后注入这条窄路也必须 // 是守卫点:SetCode / SetType 是唯一合法通道,散文进去 = 空串出来(覆盖语义)。 func TestUpstreamError_SetCodeGuardsPostConstructionInjection(t *testing.T) { t.Parallel() const prose = "Invalid value: '她刚才说的那句话'" info := UpstreamError("dashscope.example.com", 400, "application/json", nil).(*UpstreamErrorInfo) info.SetCode("Throttling.RateQuota") info.SetType("rate-limit") if info.Code != "Throttling.RateQuota" || info.Type != "rate-limit" { t.Fatalf("info.Code/Type = %q/%q,want 令牌原值", info.Code, info.Type) } // 注入不受控的散文:两个字段都变空串(而不是保留上一次的值)。 info.SetCode(prose) info.SetType(prose) if info.Code != "" || info.Type != "" { t.Fatalf("info.Code/Type = %q/%q,want 空串(SetXxx 必须过同一道守卫)", info.Code, info.Type) } if text := info.Error(); strings.Contains(text, "她刚才说的那句话") { t.Fatalf("SetCode/SetType 让散文进了错误文本:%s", text) } }