// errors 包单测(纯逻辑,无 DB/外部依赖)。 // // 重点覆盖 WithCause 的解包链契约: // WithCause(sentinel, cause) 返回的错误必须能被 errors.Is 命中**源 sentinel**, // 同时底层 cause 仍可解出。这是 T4-2 仓储层实证暴露的 latent bug 的回归保护 // (旧实现 Unwrap 只回 Cause,sentinel 副本被拷贝后丢失,errors.Is 永远 false)。 // // 包名说明:本文件位于 internal/infra/errors 包内,与包自身同名,故标准库 errors // 一律用别名 stderrors,避免与包内符号(ErrNotFound/WithCause 等)混淆。 package errors import ( stderrors "errors" "fmt" "testing" ) // TestWithCause_IsHitsBaseSentinel WithCause 的错误必须 errors.Is 命中源 sentinel。 func TestWithCause_IsHitsBaseSentinel(t *testing.T) { cause := fmt.Errorf("sql: no rows in result set") wrapped := WithCause(ErrNotFound, cause) // 源 sentinel 必须可被 errors.Is 命中(Service/Handler 层依赖此契约做分支) if !stderrors.Is(wrapped, ErrNotFound) { t.Errorf("errors.Is(WithCause(ErrNotFound, cause), ErrNotFound) = false,期望 true") } // 底层 cause 仍可达(日志排障链路不断) if !stderrors.Is(wrapped, cause) { t.Errorf("errors.Is(WithCause, cause) = false,底层 cause 丢失") } } // TestWithCause_NilBase 兜底:base 为 nil 时回退 ErrInternal 且仍可命中。 func TestWithCause_NilBase(t *testing.T) { cause := fmt.Errorf("boom") wrapped := WithCause(nil, cause) if wrapped.Code != ErrInternal.Code { t.Errorf("base=nil 应回退 ErrInternal code=%d,实际 %d", ErrInternal.Code, wrapped.Code) } if !stderrors.Is(wrapped, ErrInternal) { t.Errorf("errors.Is(WithCause(nil, cause), ErrInternal) = false,期望 true") } } // TestNew_NoBase New 创建的自定义错误:无 base、无 cause,Unwrap 返回空链。 func TestNew_NoBase(t *testing.T) { e := New(3002, "该手机号已被绑定") if e.Code != 3002 || e.Msg != "该手机号已被绑定" { t.Errorf("New 字段异常: code=%d msg=%q", e.Code, e.Msg) } if got := e.Unwrap(); len(got) != 0 { t.Errorf("New 创建的错误 Unwrap 应返回空链,实际 %v", got) } } // TestWithCause_CauseAndBaseSame 去重:把 sentinel 本身当 cause 传入,解包链只保留一份。 func TestWithCause_CauseAndBaseSame(t *testing.T) { wrapped := WithCause(ErrConflict, ErrConflict) // cause == base(同一 *AppError) chain := wrapped.Unwrap() if len(chain) != 1 { t.Errorf("cause==base 应去重为 1 项,实际 %d 项", len(chain)) } }