Files
NianAIGC/backend/internal/jobs/provider_failures_test.go
2026-10-02 19:56:32 +08:00

108 lines
5.6 KiB
Go

package jobs
import (
"context"
"encoding/json"
"strings"
"testing"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/providers"
)
func TestAllProviderRejectionsPersistSafeReasonAndNeverResubmit(t *testing.T) {
for _, provider := range []string{"seedance", "seedream", "bailian", "minimax", "evolink", "volcengine-visual"} {
t.Run(provider, func(t *testing.T) {
store := newMemoryJobStore()
job := failureFixture(provider)
store.jobs[job.ID] = job
adapter := &countingProvider{err: &providers.ProviderError{Operation: provider + " submit", Status: 400,
Code: "InputImageSensitiveContentDetected.PrivacyInformation", RequestID: "request-123",
Detail: "private prompt rejected https://private.test/input.png?token=secret Bearer secret-key"}}
processor := ProviderProcessor{Providers: ProviderRegistry{provider: adapter}, Store: store}
got, err := processor.Advance(context.Background(), job)
if err != nil || got.Status != StatusFailed || got.Error == nil || got.Error.Retryable || got.Error.Message == unknownProviderSubmissionMessage {
t.Fatalf("rejection=%#v err=%v", got, err)
}
if provider == "seedance" && !strings.Contains(got.Error.Message, "真人") {
t.Fatalf("missing portrait reason: %s", got.Error.Message)
}
if !strings.Contains(string(got.ResponsePayload), "request-123") || !strings.Contains(string(got.ResponsePayload), "PrivacyInformation") {
t.Fatalf("missing diagnostic: %s", got.ResponsePayload)
}
for _, private := range []string{"private prompt", "https://private.test", "secret-key", "token=secret"} {
if strings.Contains(string(got.ResponsePayload), private) || strings.Contains(got.Error.Message, private) {
t.Fatalf("leaked %s in failure", private)
}
}
if _, err := processor.Advance(context.Background(), got); err != nil || adapter.submits != 1 {
t.Fatalf("refused request resubmitted: %d, %v", adapter.submits, err)
}
})
}
}
func TestTerminalProviderFailureRetainsTaskAndDiscardsRawPayload(t *testing.T) {
store := newMemoryJobStore()
job := failureFixture("seedance")
job.ProviderTaskID = "existing-task"
store.jobs[job.ID] = job
adapter := &countingProvider{result: providers.Result{Status: providers.StatusFailed,
ErrorCode: "InputImageSensitiveContentDetected.PrivacyInformation", RequestID: "request-terminal",
ErrorMessage: "private prompt rejected", Raw: json.RawMessage(`{"error":{"message":"private prompt rejected","secret":"raw-secret"}}`)}}
processor := ProviderProcessor{Providers: ProviderRegistry{"seedance": adapter}, Store: store}
got, err := processor.Advance(context.Background(), job)
if err != nil || got.ProviderTaskID != "existing-task" || got.Error == nil || !strings.Contains(got.Error.Message, "真人") || got.Error.Retryable || adapter.submits != 0 {
t.Fatalf("terminal failure=%#v err=%v", got, err)
}
if strings.Contains(string(got.ResponsePayload), "private prompt") || strings.Contains(string(got.ResponsePayload), "raw-secret") || strings.Contains(string(got.ResponsePayload), `"raw"`) {
t.Fatalf("persisted unsafe raw error: %s", got.ResponsePayload)
}
}
func TestUnknownSubmitRemainsNonRetryable(t *testing.T) {
store := newMemoryJobStore()
job := failureFixture("seedance")
store.jobs[job.ID] = job
adapter := &countingProvider{err: &providers.ProviderError{Operation: "seedance submit", Cause: context.DeadlineExceeded}}
processor := ProviderProcessor{Providers: ProviderRegistry{"seedance": adapter}, Store: store}
got, err := processor.Advance(context.Background(), job)
if err != nil || got.Error == nil || got.Error.Message != unknownProviderSubmissionMessage || got.Error.Retryable {
t.Fatalf("uncertain submission=%#v err=%v", got, err)
}
if _, err := processor.Advance(context.Background(), got); err != nil || adapter.submits != 1 {
t.Fatalf("uncertain request resubmitted: %d, %v", adapter.submits, err)
}
}
func TestTransientQueryFailurePreservesTaskAndRecoversWithoutSubmit(t *testing.T) {
store := newMemoryJobStore()
job := failureFixture("bailian")
job.ProviderTaskID = "existing-task"
store.jobs[job.ID] = job
adapter := &countingProvider{err: &providers.ProviderError{Operation: "bailian query", Status: 503, RequestID: "query-123"}}
processor := ProviderProcessor{Providers: ProviderRegistry{"bailian": adapter}, Store: store}
failed, err := processor.Advance(context.Background(), job)
if err != nil || failed.Error == nil || !failed.Error.Retryable || failed.ProviderTaskID != job.ProviderTaskID {
t.Fatalf("query failure=%#v err=%v", failed, err)
}
worker := NewWorker(store, processor, nil, nil, nil, WorkerConfig{}, nil)
retry, action, err := worker.settle(context.Background(), failed)
if err != nil || action != "retry_scheduled" || retry.ProviderTaskID != "existing-task" {
t.Fatalf("retry=%#v action=%s err=%v", retry, action, err)
}
adapter.err = nil
adapter.result = providers.Result{Status: providers.StatusSucceeded, OutputURLs: []string{"https://result.test/image.png"}}
done, err := processor.Advance(context.Background(), retry)
if err != nil || done.Status != StatusSucceeded || done.Error != nil || done.ProviderTaskID != "existing-task" || adapter.submits != 0 {
t.Fatalf("recovery=%#v err=%v submits=%d", done, err, adapter.submits)
}
if strings.Contains(string(done.ResponsePayload), "providerError") {
t.Fatalf("stale failure after success: %s", done.ResponsePayload)
}
}
func failureFixture(provider string) Job {
return Job{ID: "job-failure", OwnerID: "owner", Provider: provider, Capability: "video.generate", Status: StatusQueued,
RequestPayload: json.RawMessage(`{"capability":"video.generate","prompt":"private prompt","inputUrls":["https://private.test/input.png?token=secret"]}`)}
}