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"]}`)} }