package flyto import ( "errors" "strings" "testing" ) func TestEngineError_Error(t *testing.T) { // 有 Message 时返回 Message e := &EngineError{Code: ErrAPIAuth, Message: "bad key"} if e.Error() != "bad key" { t.Errorf("Error() = %q, want 'bad key'", e.Error()) } // 无 Message 时返回 Code e2 := &EngineError{Code: ErrInternal} if e2.Error() != string(ErrInternal) { t.Errorf("Error() = %q, want %q", e2.Error(), ErrInternal) } } func TestEngineError_Unwrap(t *testing.T) { cause := errors.New("underlying") e := &EngineError{Cause: cause} if e.Unwrap() != cause { t.Errorf("Unwrap() = %v, want %v", e.Unwrap(), cause) } // 无 cause e2 := &EngineError{} if e2.Unwrap() != nil { t.Errorf("Unwrap() = %v, want nil", e2.Unwrap()) } } func TestNewEngineError(t *testing.T) { e := NewEngineError(ErrAPIAuth, "auth failed", nil) if e.Code != ErrAPIAuth { t.Errorf("Code = %q", e.Code) } if e.Message != "auth failed" { t.Errorf("Message = %q", e.Message) } if e.Suggestion == "" { t.Error("Suggestion 应从默认 map 填充") } if !strings.Contains(e.Suggestion, "API Key") { t.Errorf("Suggestion 应提到 API Key: %q", e.Suggestion) } // ErrAPIAuth 默认不可重试 if e.Retryable { t.Error("ErrAPIAuth 默认不应可重试") } } func TestNewEngineError_Retryable(t *testing.T) { // RateLimit 默认可重试 e := NewEngineError(ErrAPIRateLimit, "rate limited", nil) if !e.Retryable { t.Error("ErrAPIRateLimit 默认应可重试") } } func TestWrapError_NilCause(t *testing.T) { // nil cause 应返回新的 EngineError e := WrapError(nil, ErrInternal, "internal boom") if e.Code != ErrInternal { t.Errorf("Code = %q", e.Code) } if e.Cause != nil { t.Error("nil cause 应保持 nil") } } func TestWrapError_NormalError(t *testing.T) { cause := errors.New("network timeout") e := WrapError(cause, ErrAPIOverloaded, "overloaded") if e.Cause != cause { t.Errorf("Cause 丢失") } if e.Detail != "network timeout" { t.Errorf("Detail = %q", e.Detail) } if !e.Retryable { t.Error("ErrAPIOverloaded 应可重试") } } func TestWrapError_WrappingEngineError(t *testing.T) { // 把 EngineError 再包一层 inner := &EngineError{ Code: ErrAPIAuth, Message: "inner", Detail: "existing detail", } e := WrapError(inner, ErrInternal, "outer") if e.Code != ErrInternal { t.Errorf("Code = %q, want ErrInternal", e.Code) } // inner detail 应保留 if e.Detail != "existing detail" { t.Errorf("Detail = %q, 应保留内层的", e.Detail) } // errors.As 应该能找到内层 var found *EngineError if !errors.As(e, &found) { t.Error("errors.As 应能找到 EngineError") } } func TestIsRetryable_Nil(t *testing.T) { if IsRetryable(nil) { t.Error("nil 不应可重试") } } func TestIsRetryable_EngineError(t *testing.T) { retryable := NewEngineError(ErrAPIRateLimit, "rl", nil) if !IsRetryable(retryable) { t.Error("EngineError Retryable=true 应可重试") } nonRetryable := NewEngineError(ErrAPIAuth, "a", nil) if IsRetryable(nonRetryable) { t.Error("ErrAPIAuth 不应可重试") } } func TestIsRetryable_StringMatch(t *testing.T) { // 普通 error,通过字符串匹配 tests := []struct { msg string retryable bool }{ {"HTTP 429 Too Many Requests", true}, {"HTTP 529 Service Unavailable", true}, {"connection reset by peer", true}, {"context deadline exceeded: timeout", true}, {"HTTP 401 Unauthorized", false}, {"invalid request", false}, } for _, tt := range tests { err := errors.New(tt.msg) got := IsRetryable(err) if got != tt.retryable { t.Errorf("IsRetryable(%q) = %v, want %v", tt.msg, got, tt.retryable) } } } func TestFormatErrorForDisplay_PlainError(t *testing.T) { err := errors.New("some error") s := FormatErrorForDisplay(err, false) if !strings.Contains(s, "some error") { t.Errorf("应含原始消息: %q", s) } if !strings.Contains(s, "错误") { t.Errorf("应有'错误'前缀: %q", s) } } func TestFormatErrorForDisplay_EngineError(t *testing.T) { e := NewEngineError(ErrAPIAuth, "auth failed", nil) e.Detail = "HTTP 401" // 非 verbose 模式:不显示 Detail s := FormatErrorForDisplay(e, false) if !strings.Contains(s, "auth failed") { t.Errorf("应含 Message: %q", s) } if !strings.Contains(s, "建议") { t.Errorf("应含建议: %q", s) } if strings.Contains(s, "HTTP 401") { t.Errorf("非 verbose 不应显示 Detail: %q", s) } // verbose 模式:显示 Detail sv := FormatErrorForDisplay(e, true) if !strings.Contains(sv, "HTTP 401") { t.Errorf("verbose 应显示 Detail: %q", sv) } } func TestFormatErrorForDisplay_Retryable(t *testing.T) { e := NewEngineError(ErrAPIRateLimit, "rate limited", nil) s := FormatErrorForDisplay(e, false) if !strings.Contains(s, "自动重试") { t.Errorf("可重试错误应提示重试: %q", s) } } func TestAllErrorCodesHaveSuggestions(t *testing.T) { // 覆盖所有错误码,确保 defaultSuggestions 表完整 codes := []ErrorCode{ ErrAPIAuth, ErrAPIRateLimit, ErrAPIOverloaded, ErrAPIBadRequest, ErrToolNotFound, ErrToolExecution, ErrPermissionDenied, ErrContextOverflow, ErrBudgetExceeded, ErrMaxTurns, ErrSessionNotFound, ErrSessionClosed, ErrMCPConnection, ErrPluginLoad, ErrInternal, ErrStreamTruncated, } for _, code := range codes { e := NewEngineError(code, "test", nil) if e.Suggestion == "" { t.Errorf("%s 缺少默认 Suggestion", code) } } }