package providers import ( "context" "encoding/json" "errors" "io" "net/http" "strings" "testing" "time" ) func TestAdaptersPreserveExplicitProviderRejections(t *testing.T) { tests := []struct { name, body, wantCode, wantDetail string newAdapter func(HTTPClient) Adapter }{ {"seedance", `{"error":{"code":"InputImageSensitiveContentDetected.PrivacyInformation","message":"Input image may contain a real person. Request ID: req-safe-1"}}`, "InputImageSensitiveContentDetected.PrivacyInformation", "real person", func(c HTTPClient) Adapter { return NewSeedance(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c) }}, {"seedream", `{"error":{"code":"InvalidImage","message":"Image format is invalid"}}`, "InvalidImage", "Image format", func(c HTTPClient) Adapter { return NewSeedream(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c) }}, {"evolink", `{"data":{"error":{"code":"InvalidPrompt","message":"Prompt is invalid"}}}`, "InvalidPrompt", "Prompt is invalid", func(c HTTPClient) Adapter { return NewEvoLink(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c) }}, {"bailian", `{"code":"InvalidParameter","message":"Invalid duration"}`, "InvalidParameter", "Invalid duration", func(c HTTPClient) Adapter { return NewBailian(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c) }}, {"minimax", `{"type":"error","error":{"code":"1008","message":"Account balance is insufficient"}}`, "1008", "balance", func(c HTTPClient) Adapter { return NewMinimax(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c) }}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { for _, httpStatus := range []int{http.StatusBadRequest, http.StatusOK} { client := roundTripFunc(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: httpStatus, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.body))}, nil }) result, err := tc.newAdapter(client).Submit(context.Background(), Request{Prompt: "private prompt"}) if tc.name == "seedream" && httpStatus == http.StatusOK { if err != nil || result.Status != StatusFailed || result.ErrorCode != tc.wantCode || !strings.Contains(result.ErrorDetail, tc.wantDetail) { t.Fatalf("status=%d result=%#v err=%v", httpStatus, result, err) } continue } var failure *ProviderError if !errors.As(err, &failure) || failure.Status != httpStatus || failure.Code != tc.wantCode || !strings.Contains(failure.Detail, tc.wantDetail) { t.Fatalf("status=%d failure=%#v err=%v", httpStatus, failure, err) } if tc.name == "seedance" && failure.RequestID != "req-safe-1" { t.Fatalf("request id=%q", failure.RequestID) } } }) } } func TestSuccessfulBusinessCodesAndInformationalMessagesDoNotReject(t *testing.T) { tests := []struct { name, response, taskID string wantURL bool adapter func(HTTPClient) Adapter }{ {"evolink", `{"code":0,"message":"request accepted","data":{"id":"task-ev","status":"queued"}}`, "task-ev", false, func(c HTTPClient) Adapter { return NewEvoLink(Config{BaseURL: "https://test.invalid"}, c) }}, {"bailian", `{"code":"200","message":"task submitted","output":{"task_id":"task-ba","task_status":"PENDING"}}`, "task-ba", false, func(c HTTPClient) Adapter { return NewBailian(Config{BaseURL: "https://test.invalid"}, c) }}, {"seedance", `{"code":"success","message":"task completed","id":"task-se","status":"succeeded","content":{"video_url":"https://cdn.example/video.mp4"}}`, "task-se", true, func(c HTTPClient) Adapter { return NewSeedance(Config{BaseURL: "https://test.invalid"}, c) }}, {"minimax", `{"code":0,"message":"request accepted","task_id":"task-mi"}`, "task-mi", false, func(c HTTPClient) Adapter { return NewMinimax(Config{BaseURL: "https://test.invalid"}, c) }}, {"seedream", `{"code":0,"message":"generation complete","data":[{"url":"https://cdn.example/image.png"}]}`, "", true, func(c HTTPClient) Adapter { return NewSeedream(Config{BaseURL: "https://test.invalid"}, c) }}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := roundTripFunc(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.response))}, nil }) result, err := tc.adapter(client).Submit(context.Background(), Request{Prompt: "safe prompt"}) if err != nil || result.TaskID != tc.taskID || (tc.wantURL && len(result.OutputURLs) == 0) || (!tc.wantURL && len(result.OutputURLs) != 0) { t.Fatalf("result=%#v err=%v", result, err) } }) } } func TestAsyncTerminalFailurePersistsSafeDiagnostics(t *testing.T) { client := roundTripFunc(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"output":{"task_id":"task-1","task_status":"FAILED","code":"InvalidImage","message":"Input image was rejected"},"request_id":"req-bailian-1"}`))}, nil }) result, err := NewBailian(Config{BaseURL: "https://test.invalid", APIKey: "secret"}, client).Query(context.Background(), "task-1") if err != nil || result.Status != StatusFailed || result.ErrorCode != "InvalidImage" || result.RequestID != "req-bailian-1" || result.ErrorDetail != "Input image was rejected" { t.Fatalf("result=%#v err=%v", result, err) } encoded, err := EncodeResult(result) if err != nil { t.Fatal(err) } var persisted HTTPResult if err := json.Unmarshal(encoded, &persisted); err != nil || persisted.ErrorCode != result.ErrorCode || persisted.RequestID != result.RequestID || persisted.ErrorDetail != result.ErrorDetail { t.Fatalf("persisted=%#v err=%v", persisted, err) } } func TestAsyncAdaptersReadTerminalFailureDetails(t *testing.T) { tests := []struct { name, response, code, detail string adapter func(HTTPClient) Adapter }{ {"evolink", `{"id":"task-1","status":"failed","data":{"error":{"code":"ContentRejected","message":"Reference image rejected"}}}`, "ContentRejected", "Reference image rejected", func(c HTTPClient) Adapter { return NewEvoLink(Config{BaseURL: "https://test.invalid"}, c) }}, {"seedance", `{"id":"task-1","status":"failed","error":{"code":"InputImageSensitiveContentDetected.PrivacyInformation","message":"Input image may contain a real person"}}`, "InputImageSensitiveContentDetected.PrivacyInformation", "real person", func(c HTTPClient) Adapter { return NewSeedance(Config{BaseURL: "https://test.invalid"}, c) }}, {"minimax", `{"task":{"id":"task-1","status":"failed","error":{"code":"1026","message":"Input content rejected"}}}`, "1026", "Input content rejected", func(c HTTPClient) Adapter { return NewMinimax(Config{BaseURL: "https://test.invalid"}, c) }}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := roundTripFunc(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.response))}, nil }) result, err := tc.adapter(client).Query(context.Background(), "task-1") if err != nil || result.Status != StatusFailed || result.ErrorCode != tc.code || !strings.Contains(result.ErrorDetail, tc.detail) { t.Fatalf("result=%#v err=%v", result, err) } }) } } func TestSanitizeFailureDetailRemovesSubmittedMaterialsCredentialsAndUnknownQuotes(t *testing.T) { request := []byte(`{"prompt":"private portrait","content":[{"text":"private portrait"},{"image_url":{"url":"https://private.example/photo.png"}}],"materials":[{"url":"https://private.example/video.mp4"}],"settings":{"token":"very-secret-token"}}`) message := `invalid "private portrait" from https://private.example/photo.png; video https://private.example/video.mp4; token=very-secret-token; Bearer other-secret; field "person's private name" rejected` got := SanitizeFailureDetail(message, request, "api-secret") for _, private := range []string{"private portrait", "private.example", "very-secret-token", "other-secret", "person's private name"} { if strings.Contains(got, private) { t.Fatalf("detail leaks %q: %q", private, got) } } if !strings.Contains(got, "invalid") || !strings.Contains(got, "rejected") { t.Fatalf("detail lost context: %q", got) } if got := SanitizeFailureDetail(strings.Repeat("x", 1000), nil); len([]rune(got)) > 527 { t.Fatalf("detail unbounded: %d", len([]rune(got))) } } func TestUnknownSubmitOutcomeKeepsStatusZero(t *testing.T) { for name, client := range map[string]HTTPClient{ "transport": roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, io.ErrUnexpectedEOF }), "unreadable-response": roundTripFunc(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: 400, Header: http.Header{}, Body: &failingBody{}}, nil }), } { t.Run(name, func(t *testing.T) { _, err := NewSeedance(Config{BaseURL: "https://test.invalid"}, client).Submit(context.Background(), Request{Prompt: "test"}) var failure *ProviderError if !errors.As(err, &failure) || failure.Status != 0 { t.Fatalf("failure=%#v err=%v", failure, err) } }) } } func TestVolcengineRejectionAndTerminalFailurePreserveSafeDiagnostics(t *testing.T) { responses := []string{ `{"ResponseMetadata":{"RequestId":"req-volc-1","Error":{"Code":"50413","Message":"Input image rejected"}}}`, `{"code":10000,"Result":{"data":{"task_id":"task-volc","status":"failed","error":{"code":"InvalidImage","message":"Input image rejected"}}}}`, } call := 0 client := roundTripFunc(func(*http.Request) (*http.Response, error) { body := responses[call] call++ return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(body))}, nil }) adapter := NewVolcengine(Config{BaseURL: "https://test.invalid", AccessKeyID: "private-ak", SecretAccessKey: "private-sk"}, client, func() time.Time { return time.Unix(0, 0) }) _, err := adapter.Submit(context.Background(), Request{Prompt: "private prompt"}) var failure *ProviderError if !errors.As(err, &failure) || failure.Status != 200 || failure.Code != "50413" || failure.RequestID != "req-volc-1" || failure.Detail != "Input image rejected" { t.Fatalf("failure=%#v err=%v", failure, err) } result, err := adapter.Query(context.Background(), "task-volc") if err != nil || result.Status != StatusFailed || result.ErrorCode != "InvalidImage" || result.ErrorDetail != "Input image rejected" { t.Fatalf("terminal result=%#v err=%v", result, err) } } type failingBody struct{} func (*failingBody) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF } func (*failingBody) Close() error { return nil }