package controller import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) func TestConvertAipingNativeVideoRequestPreservesNativeContentOrder(t *testing.T) { req, err := convertAipingNativeVideoRequest([]byte(`{ "model":"doubao-seedance-2-0-260128", "content":[ {"type":"text","text":"first prompt"}, {"type":"image_url","image_url":{"url":"asset://img1"},"role":"reference_image"}, {"type":"text","text":"second prompt"}, {"type":"video_url","video_url":{"url":"https://example.test/input.mp4"},"role":"reference_video"} ], "duration":5, "resolution":"480p", "tools":[{"type":"web_search"}] }`)) require.NoError(t, err) require.Equal(t, "doubao-seedance-2-0-260128", req.Model) require.Empty(t, req.Prompt) require.Equal(t, float64(5), req.Metadata["duration"]) require.Equal(t, "480p", req.Metadata["resolution"]) require.NotNil(t, req.Metadata["tools"]) content, ok := req.Metadata["content"].([]any) require.True(t, ok) require.Len(t, content, 4) require.Equal(t, "text", content[0].(map[string]any)["type"]) require.Equal(t, "image_url", content[1].(map[string]any)["type"]) require.Equal(t, "text", content[2].(map[string]any)["type"]) require.Equal(t, "video_url", content[3].(map[string]any)["type"]) } func TestConvertAipingNativeVideoRequestPreservesArkDocumentFields(t *testing.T) { req, err := convertAipingNativeVideoRequest([]byte(`{ "model":"doubao-seedance-2-0-260128", "content":[ {"type":"text","text":"开场:海边日落"}, {"type":"image_url","image_url":{"url":"asset://image-1"},"role":"reference_image"}, {"type":"video_url","video_url":{"url":"asset://video-1"},"role":"reference_video"}, {"type":"audio_url","audio_url":{"url":"asset://audio-1"},"role":"reference_audio"}, {"type":"draft_task","draft_task":{"id":"cgt-draft"}} ], "callback_url":"https://example.test/callback", "return_last_frame":true, "service_tier":"default", "execution_expires_after":3600, "generate_audio":false, "draft":true, "tools":[{"type":"web_search"}], "safety_identifier":"user-hash-1", "priority":5, "resolution":"480p", "ratio":"1:1", "duration":5, "frames":29, "seed":11, "camera_fixed":false, "watermark":true }`)) require.NoError(t, err) require.Equal(t, "doubao-seedance-2-0-260128", req.Model) require.Empty(t, req.Prompt) require.Equal(t, "https://example.test/callback", req.Metadata["callback_url"]) require.Equal(t, true, req.Metadata["return_last_frame"]) require.Equal(t, "default", req.Metadata["service_tier"]) require.Equal(t, float64(3600), req.Metadata["execution_expires_after"]) require.Equal(t, false, req.Metadata["generate_audio"]) require.Equal(t, true, req.Metadata["draft"]) require.Equal(t, "user-hash-1", req.Metadata["safety_identifier"]) require.Equal(t, float64(5), req.Metadata["priority"]) require.Equal(t, "480p", req.Metadata["resolution"]) require.Equal(t, "1:1", req.Metadata["ratio"]) require.Equal(t, float64(5), req.Metadata["duration"]) require.Equal(t, float64(29), req.Metadata["frames"]) require.Equal(t, float64(11), req.Metadata["seed"]) require.Equal(t, false, req.Metadata["camera_fixed"]) require.Equal(t, true, req.Metadata["watermark"]) content, ok := req.Metadata["content"].([]any) require.True(t, ok) require.Len(t, content, 5) require.Equal(t, "audio_url", content[3].(map[string]any)["type"]) require.Equal(t, "draft_task", content[4].(map[string]any)["type"]) tools, ok := req.Metadata["tools"].([]any) require.True(t, ok) require.Equal(t, "web_search", tools[0].(map[string]any)["type"]) } func TestConvertAipingNativeVideoRequestRejectsMissingModel(t *testing.T) { _, err := convertAipingNativeVideoRequest([]byte(`{"content":[{"type":"text","text":"prompt"}]}`)) require.ErrorContains(t, err, "model") } func TestPrepareAipingNativeVideoSubmitStoresConvertedRequest(t *testing.T) { c := newControllerJSONContext(t, "/api/v3/contents/generations/tasks", `{ "model":"doubao-seedance-2-0-260128", "content":[{"type":"text","text":"prompt"}] }`) info := &relaycommon.RelayInfo{TaskRelayInfo: &relaycommon.TaskRelayInfo{}} err := prepareAipingNativeVideoSubmit(c, info) require.NoError(t, err) stored, err := relaycommon.GetTaskRequest(c) require.NoError(t, err) require.Equal(t, "doubao-seedance-2-0-260128", stored.Model) require.Empty(t, stored.Prompt) content, ok := stored.Metadata["content"].([]any) require.True(t, ok) require.Equal(t, "prompt", content[0].(map[string]any)["text"]) require.Equal(t, constant.TaskActionGenerate, info.Action) require.Equal(t, "doubao-seedance-2-0-260128", info.OriginModelName) } func TestBuildAipingNativeFetchResponseRewritesIDAndPreservesUpstreamFields(t *testing.T) { task := &model.Task{ TaskID: "task_public", Status: model.TaskStatusSuccess, CreatedAt: 1781496040, UpdatedAt: 1781496278, Properties: model.Properties{OriginModelName: "doubao-seedance-2-0-260128"}, Data: []byte(`{ "id":"cgt-upstream", "aiping_id":"3d2c8c17-36ad-4138-8602-88b3c60e56c6", "model":"doubao-seedance-2-0-260128", "status":"succeeded", "content":{"video_url":"https://example.test/output.mp4"}, "usage":{"completion_tokens":48400,"total_tokens":48400}, "created_at":1781496040, "updated_at":1781496278, "seed":73812, "resolution":"480p", "ratio":"1:1", "duration":5, "framespersecond":24, "service_tier":"default", "execution_expires_after":172800, "generate_audio":true, "draft":false, "priority":0 }`), } data, err := buildAipingNativeFetchResponse(task) require.NoError(t, err) var payload map[string]any require.NoError(t, common.Unmarshal(data, &payload)) require.Equal(t, "task_public", payload["id"]) require.Equal(t, "succeeded", payload["status"]) require.Equal(t, "doubao-seedance-2-0-260128", payload["model"]) require.Equal(t, "https://example.test/output.mp4", payload["content"].(map[string]any)["video_url"]) require.Equal(t, float64(48400), payload["usage"].(map[string]any)["total_tokens"]) require.Equal(t, float64(73812), payload["seed"]) require.Equal(t, "480p", payload["resolution"]) require.Equal(t, "1:1", payload["ratio"]) require.Equal(t, float64(5), payload["duration"]) require.Equal(t, float64(24), payload["framespersecond"]) require.Equal(t, "default", payload["service_tier"]) require.Equal(t, float64(172800), payload["execution_expires_after"]) require.Equal(t, true, payload["generate_audio"]) require.Equal(t, false, payload["draft"]) require.Equal(t, float64(0), payload["priority"]) require.Contains(t, payload, "error") require.Nil(t, payload["error"]) require.NotContains(t, payload, "aiping_id") } func TestBuildAipingNativeFetchResponseFallsBackForFailedTask(t *testing.T) { task := &model.Task{ TaskID: "task_public", Status: model.TaskStatusFailure, CreatedAt: 100, UpdatedAt: 200, Properties: model.Properties{OriginModelName: "doubao-seedance-2-0-260128"}, Data: []byte(`{"id":"cgt-upstream","status":"failed","error":{"code":"InvalidParameter","message":"duration is invalid"}}`), } data, err := buildAipingNativeFetchResponse(task) require.NoError(t, err) require.Contains(t, string(data), `"id":"task_public"`) require.Contains(t, string(data), `"code":"InvalidParameter"`) require.Contains(t, string(data), `"message":"duration is invalid"`) require.NotContains(t, string(data), `"error":null`) } func newControllerJSONContext(t *testing.T, path string, body string) *gin.Context { t.Helper() w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodPost, path, strings.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") return c }