package engine import ( "errors" "testing" ) func TestEngineError_Error_WithMessage(t *testing.T) { e := &EngineError{ Code: ErrAPIAuth, Message: "invalid API key", } if got := e.Error(); got != "invalid API key" { t.Errorf("Error() = %q, want %q", got, "invalid API key") } } func TestEngineError_Error_FallbackToCode(t *testing.T) { e := &EngineError{Code: ErrAPIRateLimit} if got := e.Error(); got != string(ErrAPIRateLimit) { t.Errorf("Error() = %q, want %q", got, string(ErrAPIRateLimit)) } } func TestEngineError_Unwrap(t *testing.T) { cause := errors.New("root cause") e := &EngineError{ Code: ErrInternal, Cause: cause, } if got := e.Unwrap(); got != cause { t.Errorf("Unwrap() = %v, want %v", got, cause) } // nil cause e2 := &EngineError{Code: ErrInternal} if got := e2.Unwrap(); got != nil { t.Errorf("Unwrap() = %v, want nil", got) } } func TestEngineError_ErrorsIs(t *testing.T) { cause := errors.New("root") e := &EngineError{Code: ErrToolExecution, Cause: cause} if !errors.Is(e, cause) { t.Error("errors.Is should find the wrapped cause") } } func TestWrapError_NilCause(t *testing.T) { e := WrapError(nil, ErrAPIAuth, "no key") if e.Code != ErrAPIAuth { t.Errorf("Code = %q, want %q", e.Code, ErrAPIAuth) } if e.Cause != nil { t.Error("Cause should be nil when wrapping nil") } if e.Retryable { t.Error("ErrAPIAuth should not be retryable") } } func TestWrapError_PlainError(t *testing.T) { cause := errors.New("connection timeout") e := WrapError(cause, ErrAPIOverloaded, "service busy") if e.Code != ErrAPIOverloaded { t.Errorf("Code = %q", e.Code) } if e.Message != "service busy" { t.Errorf("Message = %q", e.Message) } if e.Detail != "connection timeout" { t.Errorf("Detail = %q, want cause's Error()", e.Detail) } if !e.Retryable { t.Error("ErrAPIOverloaded should be retryable") } if e.Suggestion == "" { t.Error("Suggestion should be populated from defaultSuggestions") } } func TestWrapError_EngineError(t *testing.T) { inner := &EngineError{ Code: ErrToolExecution, Detail: "grep returned exit code 1", } outer := WrapError(inner, ErrInternal, "tool pipeline failed") if outer.Code != ErrInternal { t.Errorf("Code = %q, want ErrInternal", outer.Code) } if outer.Detail != "grep returned exit code 1" { t.Errorf("Detail = %q, should preserve inner Detail", outer.Detail) } if !outer.Retryable { t.Error("ErrInternal should be retryable") } // errors.As should find the inner EngineError var found *EngineError if !errors.As(outer.Cause, &found) { t.Error("Cause should contain the inner EngineError") } } func TestFormatErrorForDisplay_PlainError(t *testing.T) { err := errors.New("something broke") got := FormatErrorForDisplay(err, false) if got != "错误: something broke" { t.Errorf("FormatErrorForDisplay = %q", got) } } func TestFormatErrorForDisplay_EngineError(t *testing.T) { e := NewEngineError(ErrAPIAuth, "API key expired", nil) got := FormatErrorForDisplay(e, false) if got == "" { t.Fatal("FormatErrorForDisplay returned empty string") } // should contain the message and suggestion assertContains(t, got, "API key expired") assertContains(t, got, "建议:") // ErrAPIAuth is not retryable, so no retry hint assertNotContains(t, got, "可自动重试") } func TestFormatErrorForDisplay_Verbose(t *testing.T) { e := &EngineError{ Code: ErrToolExecution, Message: "grep failed", Detail: "exit code 2: no such file", Suggestion: "check the file path", Retryable: false, } // non-verbose: no detail brief := FormatErrorForDisplay(e, false) assertNotContains(t, brief, "exit code 2") // verbose: includes detail verbose := FormatErrorForDisplay(e, true) assertContains(t, verbose, "exit code 2") assertContains(t, verbose, "详情:") } func TestFormatErrorForDisplay_Retryable(t *testing.T) { e := NewEngineError(ErrAPIOverloaded, "service overloaded", nil) got := FormatErrorForDisplay(e, false) assertContains(t, got, "可自动重试") } func TestNewEngineError_DefaultSuggestions(t *testing.T) { // spot-check a few error codes get their default suggestions and retryable flags tests := []struct { code ErrorCode retryable bool }{ {ErrAPIAuth, false}, {ErrAPIRateLimit, true}, {ErrAPIOverloaded, true}, {ErrToolNotFound, false}, {ErrBudgetExceeded, false}, {ErrMCPConnection, true}, {ErrStreamTruncated, true}, } for _, tt := range tests { e := NewEngineError(tt.code, "test", nil) if e.Retryable != tt.retryable { t.Errorf("NewEngineError(%s).Retryable = %v, want %v", tt.code, e.Retryable, tt.retryable) } if e.Suggestion == "" { t.Errorf("NewEngineError(%s).Suggestion is empty", tt.code) } } } // --- helpers --- func assertContains(t *testing.T, s, substr string) { t.Helper() if len(s) == 0 || len(substr) == 0 { return } for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return } } t.Errorf("expected %q to contain %q", s, substr) } func assertNotContains(t *testing.T, s, substr string) { t.Helper() for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { t.Errorf("expected %q to NOT contain %q", s, substr) return } } }