package multiagent import ( "context" "errors" "strings" "testing" "github.com/cloudwego/eino/adk" ) func TestEinoRunErrorHandlerCancelUsesNativeFallback(t *testing.T) { pending := newEinoPendingToolCalls("conv-1", nil) pending.Mark(toolCallPendingInfo{ToolCallID: "call-1", ToolName: "execute"}) want := errors.New("native cancel") got := newEinoRunErrorHandler(einoRunErrorHandlerConfig{ ConversationID: "conv-1", Pending: pending, NativeCancelFallback: func() error { return want }, }).Handle(&adk.CancelError{Info: &adk.AgentCancelInfo{}}) if !errors.Is(got, want) { t.Fatalf("err = %v, want native fallback", got) } if pending.Count() != 0 { t.Fatalf("pending count = %d, want 0", pending.Count()) } } func TestEinoRunErrorHandlerTimeoutAndGeneralErrorProgress(t *testing.T) { for _, tc := range []struct { name string err error errorKind interface{} }{ {name: "timeout", err: context.DeadlineExceeded, errorKind: "timeout"}, {name: "general", err: errors.New("boom"), errorKind: nil}, } { t.Run(tc.name, func(t *testing.T) { var data map[string]interface{} got := newEinoRunErrorHandler(einoRunErrorHandlerConfig{ ConversationID: "conv-1", Progress: func(eventType, _ string, raw interface{}) { if eventType == "error" { data, _ = raw.(map[string]interface{}) } }, }).Handle(tc.err) if !errors.Is(got, tc.err) { t.Fatalf("err = %v", got) } if data["conversationId"] != "conv-1" || data["source"] != "eino" { t.Fatalf("data = %#v", data) } if gotKind := data["errorKind"]; gotKind != tc.errorKind { t.Fatalf("errorKind = %#v, want %#v", gotKind, tc.errorKind) } }) } } func TestEinoRunErrorHandlerRetryExhaustedEmptyOutputProgress(t *testing.T) { err := &adk.RetryExhaustedError{ LastErr: errors.New("model output rejected by ShouldRetry at attempt 5"), TotalRetries: 4, } var message string var data map[string]interface{} got := newEinoRunErrorHandler(einoRunErrorHandlerConfig{ ConversationID: "conv-1", Progress: func(eventType, msg string, raw interface{}) { if eventType == "error" { message = msg data, _ = raw.(map[string]interface{}) } }, }).Handle(err) if !errors.Is(got, err) { t.Fatalf("err = %v", got) } if !strings.Contains(message, "模型调用重试已耗尽") || !strings.Contains(message, "模型未返回原始错误;输出被重试策略拒绝。") || strings.Contains(message, "model output rejected by ShouldRetry at attempt 5") { t.Fatalf("message = %q", message) } if data["errorKind"] != "model_output_rejected" { t.Fatalf("errorKind = %#v", data["errorKind"]) } if data["errorSummary"] != "模型未返回原始错误;输出被重试策略拒绝。" { t.Fatalf("errorSummary = %#v", data["errorSummary"]) } if data["hasModelOriginalError"] != false { t.Fatalf("hasModelOriginalError = %#v", data["hasModelOriginalError"]) } if data["retryExhausted"] != true || data["totalRetries"] != 4 { t.Fatalf("retry metadata = %#v", data) } if data["lastError"] != "model output rejected by ShouldRetry at attempt 5" { t.Fatalf("lastError = %#v", data["lastError"]) } if data["technicalError"] != "model output rejected by ShouldRetry at attempt 5" { t.Fatalf("technicalError = %#v", data["technicalError"]) } if _, ok := data["modelOriginalError"]; ok { t.Fatalf("modelOriginalError should be absent for ShouldRetry rejection, got %#v", data["modelOriginalError"]) } if data["error"] != err.Error() { t.Fatalf("raw error = %#v, want %#v", data["error"], err.Error()) } } func TestEinoRunErrorHandlerRetryExhaustedOriginalErrorProgress(t *testing.T) { err := &adk.RetryExhaustedError{ LastErr: errors.New("HTTP 429 Too Many Requests"), TotalRetries: 3, } var message string var data map[string]interface{} got := newEinoRunErrorHandler(einoRunErrorHandlerConfig{ ConversationID: "conv-1", Progress: func(eventType, msg string, raw interface{}) { if eventType == "error" { message = msg data, _ = raw.(map[string]interface{}) } }, }).Handle(err) if !errors.Is(got, err) { t.Fatalf("err = %v", got) } if !strings.Contains(message, "HTTP 429 Too Many Requests") { t.Fatalf("message = %q", message) } if data["errorKind"] != "rate_limit" { t.Fatalf("errorKind = %#v", data["errorKind"]) } if data["errorSummary"] != "HTTP 429 Too Many Requests" { t.Fatalf("errorSummary = %#v", data["errorSummary"]) } if data["lastError"] != "HTTP 429 Too Many Requests" { t.Fatalf("lastError = %#v", data["lastError"]) } if data["modelOriginalError"] != "HTTP 429 Too Many Requests" { t.Fatalf("modelOriginalError = %#v", data["modelOriginalError"]) } if _, ok := data["hasModelOriginalError"]; ok { t.Fatalf("hasModelOriginalError should be absent when original error is present, got %#v", data["hasModelOriginalError"]) } } func TestEinoClientRunErrorMessageUsesRawRetryExhaustedModelError(t *testing.T) { raw := "summary content is empty: role=assistant content_runes=0 reasoning_runes=42\nprovider request id: req_123" err := &adk.RetryExhaustedError{ LastErr: errors.New(raw), TotalRetries: 4, } got := EinoClientRunErrorMessage(err) if !strings.Contains(got, "模型调用重试已耗尽(已重试 4 次)") { t.Fatalf("message missing retry prefix: %q", got) } if !strings.Contains(got, raw) { t.Fatalf("message should include raw model error:\n%s", got) } fields := EinoClientRunErrorFields(err) if fields["modelOriginalError"] != raw || fields["lastError"] != raw { t.Fatalf("raw fields = %#v", fields) } } func TestEinoClientRunErrorMessageMarksSummarizationModelRetryError(t *testing.T) { raw := `POST "https://api.deepseek.com/v1/chat/completions": 429 Too Many Requests: {"error":{"message":"Rate limit reached"}}` err := &adk.RetryExhaustedError{ LastErr: newEinoSummarizationModelError(errors.New(raw)), TotalRetries: 4, } got := EinoClientRunErrorMessage(err) for _, want := range []string{ "摘要阶段大模型调用失败", "模型调用重试已耗尽(已重试 4 次)", "最后一次大模型报错原文", raw, } { if !strings.Contains(got, want) { t.Fatalf("message missing %q:\n%s", want, got) } } if strings.Contains(got, "summarization model error") { t.Fatalf("message leaked internal wrapper:\n%s", got) } fields := EinoClientRunErrorFields(err) if fields["errorPhase"] != "summarization" || fields["summarizationModelError"] != true { t.Fatalf("phase fields = %#v", fields) } if fields["modelOriginalError"] != raw || fields["lastError"] != raw { t.Fatalf("raw fields = %#v", fields) } } func TestEinoClientRunErrorMessageMarksDirectSummarizationModelError(t *testing.T) { raw := `POST "https://api.deepseek.com/v1/chat/completions": 400 Bad Request: {"error":{"message":"invalid thinking parameter"}}` err := newEinoSummarizationModelError(errors.New(raw)) got := EinoClientRunErrorMessage(err) for _, want := range []string{ "摘要阶段大模型调用失败", "大模型报错原文", raw, } { if !strings.Contains(got, want) { t.Fatalf("message missing %q:\n%s", want, got) } } if strings.Contains(got, "summarization model error") { t.Fatalf("message leaked internal wrapper:\n%s", got) } fields := EinoClientRunErrorFields(err) if fields["errorPhase"] != "summarization" || fields["modelOriginalError"] != raw || fields["lastError"] != raw { t.Fatalf("fields = %#v", fields) } } func TestEinoRunErrorHandlerIterationLimitProgress(t *testing.T) { var events []string var errorKind interface{} err := errors.New("maximum iteration reached") got := newEinoRunErrorHandler(einoRunErrorHandlerConfig{ ConversationID: "conv-1", OrchMode: "deep", Progress: func(eventType, _ string, raw interface{}) { events = append(events, eventType) if eventType == "error" { data, _ := raw.(map[string]interface{}) errorKind = data["errorKind"] } }, }).Handle(err) if !errors.Is(got, err) { t.Fatalf("err = %v", got) } if len(events) != 2 || events[0] != "iteration_limit_reached" || events[1] != "error" { t.Fatalf("events = %#v", events) } if errorKind != "iteration_limit" { t.Fatalf("errorKind = %#v", errorKind) } } func TestEinoRunErrorHandlerNilSafe(t *testing.T) { var h *einoRunErrorHandler if h.Handle(nil) != nil { t.Fatal("nil handler nil err should return nil") } err := errors.New("boom") if got := h.Handle(err); !errors.Is(got, err) { t.Fatalf("nil handler err = %v", got) } }