diff --git a/backend/internal/app/provider_test.go b/backend/internal/app/provider_test.go index d525eb1f9..66a227f76 100644 --- a/backend/internal/app/provider_test.go +++ b/backend/internal/app/provider_test.go @@ -213,7 +213,7 @@ func TestRunVideoTaskUsesDeclarativeAgnesJSONProtocol(t *testing.T) { defer server.Close() ctx := withProtocolRegistry(context.Background(), center.registrySnapshot()) - result, err := runVideoTask(ctx, canvasGenerationInput{ + result, err := runVideoTaskForTest(ctx, canvasGenerationInput{ Mode: "video", Prompt: "make it move", Config: providerConfig{BaseURL: server.URL + "/v1", APIKey: "test-key", InterfaceType: "agnes-video", Model: "agnes-video-2.5", VideoSeconds: "5", Size: "16:9", VQuality: "720P"}, @@ -261,7 +261,7 @@ func TestRunVideoTaskDownloadsAuthenticatedDeclarativeResult(t *testing.T) { defer server.Close() ctx := withProtocolRegistry(context.Background(), center.registrySnapshot()) - result, err := runVideoTask(ctx, canvasGenerationInput{ + result, err := runVideoTaskForTest(ctx, canvasGenerationInput{ Mode: "video", Prompt: "cinematic shot", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", InterfaceType: "newapi", Model: "sora-2", VideoSeconds: "5", Size: "1280x720"}, }) @@ -871,7 +871,7 @@ func TestProviderPayloadErrorCategoryPrefersProviderCodeOverModerationWording(t func TestRunVideoTaskRequiresOfficialPluginWhenRegistryEmpty(t *testing.T) { ctx := withProtocolRegistry(context.Background(), emptyProtocolRegistry) - _, err := runVideoTask(ctx, canvasGenerationInput{ + _, err := runVideoTaskForTest(ctx, canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: "https://example.com", APIKey: "key", Model: "MiniMax-H3", InterfaceType: "minimax-video"}, }) @@ -1568,7 +1568,7 @@ func TestRunVideoTaskUsesNewAPIForAnyVideoModel(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL + "/v1", APIKey: "test-key", Model: "custom-video-v1"}, }) @@ -1585,6 +1585,46 @@ func TestRunVideoTaskUsesNewAPIForAnyVideoModel(t *testing.T) { } } +func TestRunVideoTaskRetriesTransientPollFailureWithoutRecreating(t *testing.T) { + t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true") + createCalls := 0 + pollCalls := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method + " " + r.URL.Path { + case "POST /v1/videos": + createCalls++ + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"video-1","status":"queued"}`)) + case "GET /v1/videos/video-1": + pollCalls++ + w.Header().Set("Content-Type", "application/json") + if pollCalls == 1 { + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(`{"message":"temporary outage"}`)) + return + } + _, _ = w.Write([]byte(`{"id":"video-1","status":"completed"}`)) + case "GET /v1/videos/video-1/content": + w.Header().Set("Content-Type", "video/mp4") + _, _ = w.Write([]byte("video")) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + result, err := runVideoTaskWithPolicy(context.Background(), canvasGenerationInput{ + Prompt: "make it move", + Config: providerConfig{BaseURL: server.URL + "/v1", APIKey: "test-key", Model: "custom-video-v1"}, + }, fastVideoPollPolicy()) + if err != nil { + t.Fatal(err) + } + if result["mode"] != "video" || createCalls != 1 || pollCalls != 2 { + t.Fatalf("result = %#v, create calls = %d, poll calls = %d", result, createCalls, pollCalls) + } +} + func TestRunVideoTaskSendsOnlyDeclaredResolutionName(t *testing.T) { tests := []struct { name string @@ -1628,7 +1668,7 @@ func TestRunVideoTaskSendsOnlyDeclaredResolutionName(t *testing.T) { if test.withoutProfile { profile = nil } - _, err := runVideoTask(context.Background(), canvasGenerationInput{ + _, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "synthetic prompt", Config: providerConfig{BaseURL: server.URL + "/v1", APIKey: "test-key", Model: "public-video", VideoSeconds: "8", Size: "16:9", VQuality: test.quality}, VideoCapability: profile, @@ -1670,7 +1710,7 @@ func TestRunVideoTaskUsesNestedURLBeforeResultURL(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "grok-imagine-video-1.5-1080p", VideoSeconds: "15"}, }) @@ -1721,7 +1761,7 @@ func TestRunVideoTaskUsesJSONForGrokVideo(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL + "/v1", APIKey: "test-key", Model: "grok-video", VideoSeconds: "10"}, ReferenceImages: []providerMedia{{ID: "image-1", DataURL: testReferenceImageDataURL}}, @@ -1779,7 +1819,7 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{ BaseURL: server.URL + "/v1", @@ -1821,7 +1861,7 @@ func TestRunVideoTaskXAIVideoRejectsUnreachableResultURL(t *testing.T) { })) defer server.Close() - _, err := runVideoTask(context.Background(), canvasGenerationInput{ + _, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{ BaseURL: server.URL + "/v1", @@ -2000,7 +2040,7 @@ func TestVolcengineArkVideoProtocolUsesContentTaskAndDownloadsResult(t *testing. })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL + "/api/v3", APIKey: "test-key", Model: "doubao-seedance-test", InterfaceType: "volcengine-ark-video"}, ReferenceImages: []providerMedia{{ID: "start", URL: server.URL + "/reference.png"}}, @@ -2196,7 +2236,7 @@ func TestRunNewAPIChannel1VideoTaskDownloadsSucceededObject(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL + "/v1", APIKey: "test-key", Model: "seedance-2.0", InterfaceType: "newapi-channel-1"}, }) @@ -2260,7 +2300,7 @@ func TestRunNewAPIChannel2VideoTaskDownloadsTemporaryResult(t *testing.T) { profile := DefaultModelCapabilityConfigForModel("newapi-channel-2", "grok-image-video").Video profile.Resolutions = []string{"720p"} profile.DefaultResolution = "720p" - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "grok-image-video", InterfaceType: "newapi-channel-2", VideoSeconds: "15", Size: "9:16", VQuality: "720", VideoGenerateAudio: "true"}, ReferenceImages: []providerMedia{ @@ -2315,7 +2355,7 @@ func TestRunNewAPIChannel2VideoTaskResumesOriginalProviderTaskWithoutAnotherPost ctx := withProviderAnalytics(context.Background(), nil, model.Task{ ID: "task-1", Type: "canvas_video", ProviderRequestID: "existing-provider-task", InputJSON: string(inputJSON), }) - result, err := runVideoTask(ctx, input) + result, err := runVideoTaskForTest(ctx, input) if err != nil { t.Fatalf("runVideoTask() error = %v", err) } @@ -2332,7 +2372,7 @@ func TestRunNewAPIChannel2VideoTaskReturnsTypedDeadlineWhenPollingWindowEnds(t * ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) defer cancel() ctx = withProviderAnalytics(ctx, nil, model.Task{ID: "task-1", Type: "canvas_video", ProviderRequestID: "existing-provider-task"}) - _, err := runVideoTask(ctx, canvasGenerationInput{ + _, err := runVideoTaskForTest(ctx, canvasGenerationInput{ Mode: "video", Config: providerConfig{InterfaceType: string(model.ChannelInterfaceNewAPIChannel2), Model: "video-model", BaseURL: "https://example.com", APIKey: "key"}, }) @@ -2366,7 +2406,7 @@ func TestRunGeminiVeoVideoTaskUsesLongRunningOperation(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", APIFormat: "gemini", Model: "veo-test", InterfaceType: "gemini-veo", VideoSeconds: "6", Size: "16:9", VQuality: "720"}, }) @@ -2571,7 +2611,7 @@ func TestRunNovitaVideoTaskDownloadsSucceededVideo(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "kling2.5_turbo_pro_t2v", InterfaceType: "novita-video", VideoSeconds: "5", Size: "16:9"}, }) @@ -2604,7 +2644,7 @@ func TestRunNovitaVideoTaskReturnsFailureReason(t *testing.T) { })) defer server.Close() - _, err := runVideoTask(context.Background(), canvasGenerationInput{ + _, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "kling2.5_turbo_pro_t2v", InterfaceType: "novita-video"}, }) @@ -2650,7 +2690,7 @@ func TestRunMiniMaxVideoTaskCreatesPollsAndDownloads(t *testing.T) { })) defer server.Close() - result, err := runVideoTask(context.Background(), canvasGenerationInput{ + result, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Mode: "video", Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "MiniMax-H3", InterfaceType: "minimax-video", VideoSeconds: "5", VQuality: "768P", Size: "16:9"}, @@ -2686,7 +2726,7 @@ func TestRunMiniMaxVideoTaskSendsOfficial2KResolution(t *testing.T) { _, _ = w.Write([]byte(`{}`)) })) defer server.Close() - _, err := runVideoTask(context.Background(), canvasGenerationInput{ + _, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Mode: "video", Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "MiniMax-H3", InterfaceType: "minimax-video", VideoSeconds: "6", VQuality: "2K"}, }) @@ -2731,7 +2771,7 @@ func TestRunMiniMaxVideoTaskUsesExplicitReferenceRoles(t *testing.T) { })) defer server.Close() - _, err := runVideoTask(context.Background(), canvasGenerationInput{ + _, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Mode: "video", Prompt: "保持角色一致", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "MiniMax-H3", InterfaceType: "minimax-video", VideoSeconds: "6", VQuality: "768P", Size: "16:9"}, @@ -2760,7 +2800,7 @@ func TestRunMiniMaxVideoTaskReturnsFailureReason(t *testing.T) { })) defer server.Close() - _, err := runVideoTask(context.Background(), canvasGenerationInput{ + _, err := runVideoTaskForTest(context.Background(), canvasGenerationInput{ Mode: "video", Prompt: "make it move", Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "MiniMax-H3", InterfaceType: "minimax-video", VideoSeconds: "6"}, diff --git a/backend/internal/app/provider_video.go b/backend/internal/app/provider_video.go index 05a4203b4..7d73edc3f 100644 --- a/backend/internal/app/provider_video.go +++ b/backend/internal/app/provider_video.go @@ -10,7 +10,6 @@ import ( "mime/multipart" "strconv" "strings" - "time" "infinite-canvas/backend/internal/model" ) @@ -52,6 +51,10 @@ func (s *Service) validateResolvedVideoCapability(input *canvasGenerationInput) } func runVideoTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { + return runVideoTaskWithPolicy(ctx, input, defaultVideoPollPolicy()) +} + +func runVideoTaskWithPolicy(ctx context.Context, input canvasGenerationInput, pollPolicy videoPollPolicy) (map[string]interface{}, error) { // 路由顺序是协议边界,不是“哪个请求先试”:官方声明式接口必须由已注册适配器执行, // 缺少适配器时直接失败,不能偷偷退回遗留手写协议;只有未声明为官方插件的旧渠道才继续走兼容分支。 if strings.TrimSpace(input.Mode) == "" { @@ -60,17 +63,17 @@ func runVideoTask(ctx context.Context, input canvasGenerationInput) (map[string] // 已有官方声明式插件的 InterfaceType 只走适配器。未注入 registry 时补官方包, // 显式空 registry 则报“插件未安装”,不再回退到手写协议。 ctx = ensureOfficialProtocolAdapter(ctx, input.Config.InterfaceType) - if _, ok := declarativeProtocolAdapterForContext(ctx, input.Config.InterfaceType); ok { - return runDeclarativeProtocolTask(ctx, input) + if adapter, ok := declarativeProtocolAdapterForContext(ctx, input.Config.InterfaceType); ok { + return runProtocolAdapterTaskWithPolicy(ctx, input, adapter, pollPolicy) } if label, official := officialDeclarativeVideoInterface(input.Config.InterfaceType); official { return nil, fmt.Errorf("%s 视频插件未安装", label) } if isArkPlanVideoConfig(input.Config) { - return runSeedanceAgentPlanVideoTask(ctx, input) + return runSeedanceAgentPlanVideoTask(ctx, input, pollPolicy) } if isSeedanceVideoConfig(input.Config) { - return runSeedanceVideosTask(ctx, input) + return runSeedanceVideosTask(ctx, input, pollPolicy) } if len(input.ReferenceVideos) > 0 || len(input.ReferenceAudios) > 0 { return nil, errors.New("OpenAI 风格视频接口不支持参考视频或参考音频,请切换到 Seedance / Agent Plan 渠道") @@ -131,10 +134,10 @@ func runVideoTask(ctx context.Context, input canvasGenerationInput) (map[string] if id == "" { return nil, errors.New("视频接口没有返回任务 ID") } - for deadline := providerPollingDeadline(ctx); time.Now().Before(deadline); { + return runVideoPollLoop(ctx, id, pollPolicy, func(ctx context.Context) (videoPollOutcome, error) { var state map[string]interface{} if err := getJSON(ctx, input.Config, "/videos/"+id, &state); err != nil { - return nil, err + return videoPollOutcome{}, err } if data, ok := state["data"].(map[string]interface{}); ok { state = data @@ -144,25 +147,22 @@ func runVideoTask(ctx context.Context, input canvasGenerationInput) (map[string] if videoURL := newAPIVideoResultURL(state); videoURL != "" { data, mimeType, err := getProviderExternalBinary(withProviderRequestKind(ctx, "download"), input.Config, videoURL) if err != nil { - return nil, fmt.Errorf("视频结果下载失败(任务 %s):%w", id, err) + return videoPollOutcome{}, fmt.Errorf("视频结果下载失败(任务 %s):%w", id, err) } mimeType = normalizedMediaMimeType(mimeType, data) - return map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}, nil + return videoPollOutcome{Done: true, Result: map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}}, nil } data, mimeType, err := getBinary(ctx, input.Config, "/videos/"+id+"/content") if err != nil { - return nil, err + return videoPollOutcome{}, err } - return map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}, nil + return videoPollOutcome{Done: true, Result: map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}}, nil } if status == "failed" || status == "cancelled" { - return nil, errors.New("视频生成失败") + return videoPollOutcome{}, errors.New("视频生成失败") } - if err := sleepContext(ctx, 2500*time.Millisecond); err != nil { - return nil, err - } - } - return nil, errors.New("视频生成超时") + return videoPollOutcome{}, nil + }) } func newAPIVideoResultURL(state map[string]interface{}) string { @@ -221,7 +221,7 @@ func grokVideoBody(input canvasGenerationInput) (map[string]interface{}, error) return body, nil } -func runSeedanceVideosTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { +func runSeedanceVideosTask(ctx context.Context, input canvasGenerationInput, pollPolicy videoPollPolicy) (map[string]interface{}, error) { // 恢复任务已有 provider ID 时只能继续查询,绝不能重新 create,否则会产生第二个计费任务。 // create 成功后的 poll/download 任一失败都保留失败或结果未知语义,不降级成“成功但无内容”。 id := resumedProviderRequestID(ctx) @@ -246,10 +246,10 @@ func runSeedanceVideosTask(ctx context.Context, input canvasGenerationInput) (ma if id == "" { return nil, errors.New("Seedance 接口没有返回任务 ID") } - for deadline := providerPollingDeadline(ctx); time.Now().Before(deadline); { + return runVideoPollLoop(ctx, id, pollPolicy, func(ctx context.Context) (videoPollOutcome, error) { var state map[string]interface{} if err := getJSON(ctx, input.Config, "/videos/"+id, &state); err != nil { - return nil, err + return videoPollOutcome{}, err } if data, ok := state["data"].(map[string]interface{}); ok { state = data @@ -260,27 +260,24 @@ func runSeedanceVideosTask(ctx context.Context, input canvasGenerationInput) (ma if videoURL != "" { data, mimeType, err := getExternalBinary(withProviderRequestKind(ctx, "download"), videoURL) if err != nil { - return nil, fmt.Errorf("视频结果下载失败:%w", err) + return videoPollOutcome{}, fmt.Errorf("视频结果下载失败:%w", err) } - return map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}, nil + return videoPollOutcome{Done: true, Result: map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}}, nil } data, mimeType, err := getBinary(ctx, input.Config, "/videos/"+id+"/content") if err != nil { - return nil, fmt.Errorf("Seedance 任务成功但未返回视频 URL,备用内容下载失败:%w", err) + return videoPollOutcome{}, fmt.Errorf("Seedance 任务成功但未返回视频 URL,备用内容下载失败:%w", err) } - return map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}, nil + return videoPollOutcome{Done: true, Result: map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}}, nil } if status == "failed" || status == "cancelled" || status == "expired" { - return nil, errors.New(defaultString(seedanceErrorMessage(state), "Seedance 视频生成失败")) + return videoPollOutcome{}, errors.New(defaultString(seedanceErrorMessage(state), "Seedance 视频生成失败")) } - if err := sleepContext(ctx, 5*time.Second); err != nil { - return nil, err - } - } - return nil, errors.New("Seedance 视频生成超时") + return videoPollOutcome{}, nil + }) } -func runSeedanceAgentPlanVideoTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { +func runSeedanceAgentPlanVideoTask(ctx context.Context, input canvasGenerationInput, pollPolicy videoPollPolicy) (map[string]interface{}, error) { providerName := "Seedance" if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineArkVideo) { providerName = "火山方舟" @@ -331,10 +328,10 @@ func runSeedanceAgentPlanVideoTask(ctx context.Context, input canvasGenerationIn if id == "" { return nil, fmt.Errorf("%s接口没有返回任务 ID", providerName) } - for deadline := providerPollingDeadline(ctx); time.Now().Before(deadline); { + return runVideoPollLoop(ctx, id, pollPolicy, func(ctx context.Context) (videoPollOutcome, error) { var state map[string]interface{} if err := getJSON(ctx, input.Config, "/contents/generations/tasks/"+id, &state); err != nil { - return nil, err + return videoPollOutcome{}, err } if data, ok := state["data"].(map[string]interface{}); ok { state = data @@ -344,22 +341,19 @@ func runSeedanceAgentPlanVideoTask(ctx context.Context, input canvasGenerationIn content, _ := state["content"].(map[string]interface{}) videoURL := stringField(content, "video_url") if videoURL == "" { - return nil, fmt.Errorf("%s任务成功但没有返回视频 URL", providerName) + return videoPollOutcome{}, fmt.Errorf("%s任务成功但没有返回视频 URL", providerName) } data, mimeType, err := getExternalBinary(withProviderRequestKind(ctx, "download"), videoURL) if err != nil { - return nil, fmt.Errorf("视频结果下载失败:%w", err) + return videoPollOutcome{}, fmt.Errorf("视频结果下载失败:%w", err) } - return map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}, nil + return videoPollOutcome{Done: true, Result: map[string]interface{}{"mode": "video", "video": map[string]interface{}{"dataUrl": dataURL(mimeType, data), "mimeType": mimeType}}}, nil } if status == "failed" || status == "cancelled" || status == "expired" { - return nil, fmt.Errorf("%s视频生成失败", providerName) + return videoPollOutcome{}, fmt.Errorf("%s视频生成失败", providerName) } - if err := sleepContext(ctx, 5*time.Second); err != nil { - return nil, err - } - } - return nil, fmt.Errorf("%s视频生成超时", providerName) + return videoPollOutcome{}, nil + }) } func seedanceContent(input canvasGenerationInput) ([]map[string]interface{}, error) { diff --git a/backend/internal/app/provider_video_polling_test.go b/backend/internal/app/provider_video_polling_test.go index 6af234654..1f778e41e 100644 --- a/backend/internal/app/provider_video_polling_test.go +++ b/backend/internal/app/provider_video_polling_test.go @@ -127,3 +127,7 @@ func fastVideoPollPolicy() videoPollPolicy { policy.Sleep = func(context.Context, time.Duration) error { return nil } return policy } + +func runVideoTaskForTest(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { + return runVideoTaskWithPolicy(ctx, input, fastVideoPollPolicy()) +}