easyai-ai-gateway/apps/api/internal/clients/clients_test.go

672 lines
25 KiB
Go

package clients
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestSimulationClientReturnsImageDemoAssets(t *testing.T) {
response, err := (SimulationClient{}).Run(context.Background(), Request{
Kind: "images.generations",
Model: "gpt-image-1",
Body: map[string]any{
"prompt": "demo image",
"n": 2,
"simulationDurationMs": 5,
},
Candidate: store.RuntimeModelCandidate{Provider: "simulation"},
})
if err != nil {
t.Fatalf("run simulation image client: %v", err)
}
data, _ := response.Result["data"].([]any)
if len(data) != 2 || response.ResponseDurationMS <= 0 {
t.Fatalf("unexpected simulated image response: %+v duration=%d", response.Result, response.ResponseDurationMS)
}
item, _ := data[0].(map[string]any)
if item["url"] != "/static/simulation/image.svg" || item["assetSource"] != "simulation" {
t.Fatalf("unexpected simulated image item: %+v", item)
}
}
func TestSimulationClientReturnsVideoDemoAssets(t *testing.T) {
response, err := (SimulationClient{}).Run(context.Background(), Request{
Kind: "videos.generations",
ModelType: "video_generate",
Model: "demo-video-model",
Body: map[string]any{
"prompt": "demo video",
"count": 2,
"duration": 6,
"simulationDurationMs": 5,
},
Candidate: store.RuntimeModelCandidate{Provider: "simulation"},
})
if err != nil {
t.Fatalf("run simulation video client: %v", err)
}
data, _ := response.Result["data"].([]any)
if len(data) != 2 || response.ResponseDurationMS <= 0 {
t.Fatalf("unexpected simulated video response: %+v duration=%d", response.Result, response.ResponseDurationMS)
}
item, _ := data[0].(map[string]any)
if item["video_url"] != "/static/simulation/video.mp4" || item["url"] != "/static/simulation/video.mp4" || item["poster"] != "/static/simulation/video-poster.svg" {
t.Fatalf("unexpected simulated video item: %+v", item)
}
if item["duration"] != 6 || item["assetSource"] != "simulation" {
t.Fatalf("unexpected simulated video metadata: %+v", item)
}
}
func TestSimulationDurationDefaultsByMediaType(t *testing.T) {
imageDuration := simulationDuration(Request{Kind: "images.generations"})
if imageDuration < 10*time.Second || imageDuration > 30*time.Second {
t.Fatalf("image simulation duration should default to 10-30s, got %s", imageDuration)
}
videoDuration := simulationDuration(Request{Kind: "videos.generations"})
if videoDuration < 2*time.Minute || videoDuration > 3*time.Minute {
t.Fatalf("video simulation duration should default to 2-3m, got %s", videoDuration)
}
textDuration := simulationDuration(Request{Kind: "chat.completions"})
if textDuration < 800*time.Millisecond || textDuration > 2400*time.Millisecond {
t.Fatalf("text simulation duration should keep short defaults, got %s", textDuration)
}
}
func TestSimulationDurationCanBeControlledByParams(t *testing.T) {
fixedDuration := simulationDuration(Request{Body: map[string]any{"simulationDurationSeconds": 7}})
if fixedDuration != 7*time.Second {
t.Fatalf("simulationDurationSeconds should set fixed duration, got %s", fixedDuration)
}
rangeDuration := simulationDuration(Request{
Kind: "videos.generations",
Body: map[string]any{
"simulationMinDurationSeconds": 1,
"simulationMaxDurationSeconds": 1,
},
})
if rangeDuration != time.Second {
t.Fatalf("simulation duration range params should override video defaults, got %s", rangeDuration)
}
}
func TestOpenAIClientChatContract(t *testing.T) {
var gotPath string
var gotAuth string
var gotModel string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
w.Header().Set("X-Request-Id", "req-chat-test")
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotModel, _ = body["model"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "chatcmpl-test",
"object": "chat.completion",
"model": gotModel,
"choices": []any{map[string]any{
"message": map[string]any{"role": "assistant", "content": "ok"},
}},
"usage": map[string]any{"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5},
})
}))
defer server.Close()
response, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "chat.completions",
Model: "openai:gpt-4o-mini",
Body: map[string]any{"model": "openai:gpt-4o-mini", "messages": []any{map[string]any{"role": "user", "content": "ping"}}},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "gpt-4o-mini",
ProviderModelName: "openai-compatible-gpt-4o-mini",
Credentials: map[string]any{"apiKey": "test-key"},
},
})
if err != nil {
t.Fatalf("run openai client: %v", err)
}
if gotPath != "/chat/completions" || gotAuth != "Bearer test-key" || gotModel != "openai-compatible-gpt-4o-mini" {
t.Fatalf("unexpected request path=%s auth=%s model=%s", gotPath, gotAuth, gotModel)
}
if response.Usage.TotalTokens != 5 || response.Result["id"] != "chatcmpl-test" {
t.Fatalf("unexpected response: %+v", response)
}
if response.RequestID != "req-chat-test" || response.ResponseStartedAt.IsZero() || response.ResponseFinishedAt.IsZero() {
t.Fatalf("response metadata was not captured: %+v", response)
}
}
func TestOpenAIClientChatStreamContract(t *testing.T) {
var gotStream bool
var gotIncludeUsage bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotStream, _ = body["stream"].(bool)
streamOptions, _ := body["stream_options"].(map[string]any)
gotIncludeUsage, _ = streamOptions["include_usage"].(bool)
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-stream\",\"object\":\"chat.completion.chunk\",\"model\":\"deepseek-v4-flash\",\"choices\":[{\"delta\":{\"content\":\"hello\"}}],\"usage\":null}\n\n"))
_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-stream\",\"object\":\"chat.completion.chunk\",\"model\":\"deepseek-v4-flash\",\"choices\":[{\"delta\":{\"content\":\" world\"},\"finish_reason\":\"stop\"}],\"usage\":null}\n\n"))
_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-stream\",\"object\":\"chat.completion.chunk\",\"model\":\"deepseek-v4-flash\",\"choices\":[],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2,\"total_tokens\":3}}\n\n"))
_, _ = w.Write([]byte("data: [DONE]\n\n"))
}))
defer server.Close()
response, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "chat.completions",
Model: "DeepSeek-V4-Flash",
Body: map[string]any{
"model": "DeepSeek-V4-Flash",
"messages": []any{map[string]any{"role": "user", "content": "ping"}},
"stream": true,
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "deepseek-v4-flash",
Credentials: map[string]any{"apiKey": "test-key"},
},
})
if err != nil {
t.Fatalf("run openai stream client: %v", err)
}
if !gotStream {
t.Fatalf("expected upstream stream request")
}
if !gotIncludeUsage {
t.Fatalf("expected upstream stream_options.include_usage=true")
}
if response.Usage.TotalTokens != 3 {
t.Fatalf("unexpected usage: %+v", response.Usage)
}
choices, _ := response.Result["choices"].([]any)
choice, _ := choices[0].(map[string]any)
message, _ := choice["message"].(map[string]any)
if message["content"] != "hello world" {
t.Fatalf("unexpected stream response: %+v", response.Result)
}
}
func TestGeminiClientChatContract(t *testing.T) {
var gotPath string
var gotKey string
var gotText string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotKey = r.URL.Query().Get("key")
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
contents, _ := body["contents"].([]any)
first, _ := contents[0].(map[string]any)
parts, _ := first["parts"].([]any)
part, _ := parts[0].(map[string]any)
gotText, _ = part["text"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{
"candidates": []any{map[string]any{
"content": map[string]any{
"parts": []any{map[string]any{"text": "gemini ok"}},
},
}},
"usageMetadata": map[string]any{
"promptTokenCount": 4,
"candidatesTokenCount": 6,
"totalTokenCount": 10,
},
})
}))
defer server.Close()
response, err := (GeminiClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "chat.completions",
Model: "gemini:gemini-2.5-flash",
Body: map[string]any{
"model": "gemini:gemini-2.5-flash",
"messages": []any{map[string]any{"role": "user", "content": "ping"}},
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "gemini-2.5-flash",
ProviderModelName: "gemini-compatible-2.5-flash",
ModelType: "chat",
Credentials: map[string]any{"apiKey": "gemini-key"},
},
})
if err != nil {
t.Fatalf("run gemini client: %v", err)
}
if gotPath != "/v1beta/models/gemini-compatible-2.5-flash:generateContent" || gotKey != "gemini-key" || gotText != "ping" {
t.Fatalf("unexpected request path=%s key=%s text=%s", gotPath, gotKey, gotText)
}
if response.Usage.TotalTokens != 10 || extractText(response.Result) != "gemini ok" {
t.Fatalf("unexpected response: %+v", response)
}
}
func TestGeminiURLAcceptsVersionedBaseURL(t *testing.T) {
got := geminiURL("https://generativelanguage.googleapis.com/v1beta", "gemini-2.5-flash", "test-key")
want := "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key"
if got != want {
t.Fatalf("unexpected gemini url: %s", got)
}
}
func TestVolcesClientImageEditUsesGenerationEndpoint(t *testing.T) {
var gotPath string
var gotAuth string
var gotModel string
var gotImage string
var gotSequential string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotModel, _ = body["model"].(string)
gotImage, _ = body["image"].(string)
gotSequential, _ = body["sequential_image_generation"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "img-volces-edit",
"created": 123,
"data": []any{map[string]any{"url": "https://example.com/out.png"}},
})
}))
defer server.Close()
response, err := (VolcesClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "images.edits",
ModelType: "image_edit",
Model: "doubao-4.0图像编辑",
Body: map[string]any{
"model": "doubao-4.0图像编辑",
"prompt": "make it brighter",
"image": "https://example.com/source.png",
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "豆包Seedream-4.0",
ProviderModelName: "doubao-seedream-4-0-250828",
Credentials: map[string]any{"apiKey": "volces-key"},
Capabilities: map[string]any{
"image_edit": map[string]any{"output_multiple_images": true},
},
},
})
if err != nil {
t.Fatalf("run volces image edit: %v", err)
}
if gotPath != "/images/generations" || gotAuth != "Bearer volces-key" {
t.Fatalf("unexpected request path=%s auth=%s", gotPath, gotAuth)
}
if gotModel != "doubao-seedream-4-0-250828" || gotImage != "https://example.com/source.png" || gotSequential != "auto" {
t.Fatalf("unexpected body model=%s image=%s sequential=%s", gotModel, gotImage, gotSequential)
}
if response.Result["id"] != "img-volces-edit" {
t.Fatalf("unexpected response: %+v", response.Result)
}
}
func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
var submitPath string
var pollPath string
var gotAuth string
var gotModel string
var gotText string
var gotFirstFrameRole string
var gotDuration float64
var gotRatio string
var gotResolution string
var gotSeed float64
var gotCameraFixed bool
var gotWatermark bool
var submittedRemoteTaskID string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
switch r.Method + " " + r.URL.Path {
case "POST /contents/generations/tasks":
submitPath = r.URL.Path
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotModel, _ = body["model"].(string)
if body["prompt"] != nil || body["first_frame"] != nil {
t.Fatalf("video convenience fields leaked upstream: %+v", body)
}
for _, key := range []string{"duration_seconds", "aspect_ratio", "audio", "cameraFixed"} {
if _, ok := body[key]; ok {
t.Fatalf("volces video task body should not include top-level %s: %+v", key, body)
}
}
gotDuration, _ = body["duration"].(float64)
gotRatio, _ = body["ratio"].(string)
gotResolution, _ = body["resolution"].(string)
gotSeed, _ = body["seed"].(float64)
gotCameraFixed, _ = body["camera_fixed"].(bool)
gotWatermark, _ = body["watermark"].(bool)
content, _ := body["content"].([]any)
textItem, _ := content[0].(map[string]any)
gotText, _ = textItem["text"].(string)
frameItem, _ := content[1].(map[string]any)
gotFirstFrameRole, _ = frameItem["role"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{"id": "cgt-test"})
case "GET /contents/generations/tasks/cgt-test":
pollPath = r.URL.Path
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "cgt-test",
"model": "doubao-seedance-2-0-260128",
"status": "succeeded",
"created_at": 456,
"content": map[string]any{"video_url": "https://example.com/out.mp4"},
"usage": map[string]any{"completion_tokens": 7, "total_tokens": 9},
})
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
}))
defer server.Close()
response, err := (VolcesClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "videos.generations",
ModelType: "video_generate",
Model: "豆包Seedance-2.0",
Body: map[string]any{
"model": "豆包Seedance-2.0",
"prompt": "A clean product reveal",
"first_frame": "https://example.com/first.png",
"duration": 6,
"aspect_ratio": "16:9",
"resolution": "720p",
"seed": 11,
"cameraFixed": false,
"watermark": true,
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "豆包Seedance-2.0",
ProviderModelName: "doubao-seedance-2-0-260128",
Credentials: map[string]any{"apiKey": "volces-key"},
PlatformConfig: map[string]any{
"volcesPollIntervalMs": 100,
"volcesPollTimeoutSeconds": 1,
},
},
OnRemoteTaskSubmitted: func(remoteTaskID string, payload map[string]any) error {
submittedRemoteTaskID = remoteTaskID
if payload["id"] != "cgt-test" {
t.Fatalf("unexpected submitted payload: %+v", payload)
}
return nil
},
})
if err != nil {
t.Fatalf("run volces video: %v", err)
}
if submitPath != "/contents/generations/tasks" || pollPath != "/contents/generations/tasks/cgt-test" || gotAuth != "Bearer volces-key" {
t.Fatalf("unexpected paths/auth submit=%s poll=%s auth=%s", submitPath, pollPath, gotAuth)
}
if submittedRemoteTaskID != "cgt-test" {
t.Fatalf("remote task submit callback did not receive task id, got %q", submittedRemoteTaskID)
}
if gotModel != "doubao-seedance-2-0-260128" || gotFirstFrameRole != "first_frame" {
t.Fatalf("unexpected submitted model=%s role=%s", gotModel, gotFirstFrameRole)
}
if gotText != "A clean product reveal" {
t.Fatalf("video params should not be appended to prompt text, got %q", gotText)
}
if gotDuration != 6 || gotRatio != "16:9" || gotResolution != "720p" || gotSeed != 11 || gotCameraFixed != false || gotWatermark != true {
t.Fatalf("unexpected submitted video params duration=%v ratio=%s resolution=%s seed=%v camera_fixed=%v watermark=%v", gotDuration, gotRatio, gotResolution, gotSeed, gotCameraFixed, gotWatermark)
}
data, _ := response.Result["data"].([]any)
item, _ := data[0].(map[string]any)
if item["url"] != "https://example.com/out.mp4" || response.Usage.TotalTokens != 9 {
t.Fatalf("unexpected response: %+v usage=%+v", response.Result, response.Usage)
}
}
func TestVolcesClientVideoRejectsDuplicateFirstFrameBeforeSubmit(t *testing.T) {
var submitted bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
submitted = true
t.Fatalf("duplicate first_frame request should not be submitted upstream")
}))
defer server.Close()
_, err := (VolcesClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "videos.generations",
ModelType: "image_to_video",
Model: "豆包Seedance",
Body: map[string]any{
"model": "豆包Seedance",
"content": []any{
map[string]any{"type": "text", "text": "animate it"},
map[string]any{"type": "image_url", "role": "first_frame", "image_url": map[string]any{"url": "https://example.com/first.png"}},
map[string]any{"type": "image_url", "role": "first_frame", "image_url": map[string]any{"url": "https://example.com/second.png"}},
},
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ProviderModelName: "doubao-seedance-1-5-pro-251215",
Credentials: map[string]any{"apiKey": "volces-key"},
},
})
if err == nil || ErrorCode(err) != "invalid_parameter" {
t.Fatalf("expected local invalid_parameter error, got %v", err)
}
if submitted {
t.Fatal("request was submitted upstream")
}
}
func TestVolcesVideoBodyAllowsOnlyTaskPayloadFields(t *testing.T) {
body := volcesVideoBody(Request{
Kind: "videos.generations",
ModelType: "omni_video",
Model: "豆包Seedance",
Body: map[string]any{
"model": "豆包Seedance",
"duration": 8,
"duration_seconds": 8,
"aspect_ratio": "9:16",
"resolution": "720p",
"audio": true,
"callback_url": "https://example.com/callback",
"returnLastFrame": true,
"executionExpiresAfter": 3600,
"draft": false,
"cameraFixed": false,
"watermark": true,
"seed": -1,
"task_id": "local-task-id",
"runMode": "simulation",
"fps": 24,
"content": []any{
map[string]any{"type": "text", "text": "Use <<<element_1>>> in a product reveal"},
map[string]any{
"type": "element",
"element": map[string]any{
"inline_element": map[string]any{
"name": "subject",
"frontal_image_url": "https://example.com/subject.png",
"refer_images": []any{map[string]any{"url": "https://example.com/side.png", "slot_key": "side"}},
},
},
},
map[string]any{
"type": "image_url",
"role": "unexpected_role",
"name": "drop-me",
"image_url": map[string]any{"url": "https://example.com/ref.png", "extra": "drop-me"},
},
map[string]any{
"type": "video_url",
"duration": 3,
"video_url": map[string]any{
"url": "https://example.com/ref.mp4",
"refer_type": "feature",
"keep_original_sound": "yes",
"extra": "drop-me",
},
},
map[string]any{
"type": "audio_url",
"audio_url": map[string]any{"url": "https://example.com/ref.mp3", "extra": "drop-me"},
},
},
},
Candidate: store.RuntimeModelCandidate{
ModelName: "豆包Seedance",
ProviderModelName: "doubao-seedance-2-0-260128",
Credentials: map[string]any{"apiKey": "volces-key"},
},
})
allowedTopLevel := map[string]bool{
"model": true, "content": true, "callback_url": true, "return_last_frame": true, "execution_expires_after": true,
"generate_audio": true, "draft": true, "resolution": true, "ratio": true, "duration": true,
"seed": true, "camera_fixed": true, "watermark": true,
}
for key := range body {
if !allowedTopLevel[key] {
t.Fatalf("unexpected top-level volces field %q in %+v", key, body)
}
}
if body["model"] != "doubao-seedance-2-0-260128" ||
body["generate_audio"] != true ||
body["callback_url"] != "https://example.com/callback" ||
body["return_last_frame"] != true ||
body["execution_expires_after"] != 3600 ||
body["draft"] != false ||
body["resolution"] != "720p" ||
body["ratio"] != "9:16" ||
body["duration"] != 8 ||
body["seed"] != -1 ||
body["camera_fixed"] != false ||
body["watermark"] != true {
t.Fatalf("unexpected direct video fields: %+v", body)
}
content, ok := body["content"].([]map[string]any)
if !ok || len(content) != 5 {
t.Fatalf("unexpected sanitized content: %#v", body["content"])
}
text := content[0]
if text["type"] != "text" || strings.Contains(text["text"].(string), "--dur") || strings.Contains(text["text"].(string), "--ratio") {
t.Fatalf("video params should not be appended to the text item: %+v", text)
}
elementImage := content[1]
if elementImage["type"] != "image_url" || elementImage["role"] != "reference_image" {
t.Fatalf("referenced element should be converted to reference image: %+v", elementImage)
}
imageURL, _ := elementImage["image_url"].(map[string]any)
if imageURL["url"] != "https://example.com/subject.png" || len(imageURL) != 1 {
t.Fatalf("element image payload should only include url: %+v", imageURL)
}
referenceImage := content[2]
if referenceImage["role"] != "reference_image" || referenceImage["name"] != nil {
t.Fatalf("image references should be role-normalized and scrubbed: %+v", referenceImage)
}
videoItem := content[3]
videoURL, _ := videoItem["video_url"].(map[string]any)
if videoItem["role"] != "reference_video" || videoURL["url"] != "https://example.com/ref.mp4" || videoURL["refer_type"] != "feature" || videoURL["extra"] != nil {
t.Fatalf("video references should keep only allowed nested fields: %+v", videoItem)
}
audioItem := content[4]
audioURL, _ := audioItem["audio_url"].(map[string]any)
if audioItem["role"] != "reference_audio" || audioURL["url"] != "https://example.com/ref.mp3" || len(audioURL) != 1 {
t.Fatalf("audio references should keep only url: %+v", audioItem)
}
}
func TestVolcesVideoBodyPrefersFramesOverDuration(t *testing.T) {
body := volcesVideoBody(Request{
Kind: "videos.generations",
ModelType: "video_generate",
Body: map[string]any{
"prompt": "A quick camera move",
"duration": 8,
"frames": 57,
},
Candidate: store.RuntimeModelCandidate{
ProviderModelName: "doubao-seedance-1-0-pro-250528",
},
})
if body["frames"] != 57 {
t.Fatalf("frames should be passed through as the official duration control: %+v", body)
}
if _, ok := body["duration"]; ok {
t.Fatalf("duration should not be sent when frames is present: %+v", body)
}
}
func TestVolcesClientVideoResumePollsExistingTaskID(t *testing.T) {
var submitCalled bool
var pollPath string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method + " " + r.URL.Path {
case "POST /contents/generations/tasks":
submitCalled = true
t.Fatalf("resume should skip upstream submit when remote task id exists")
case "GET /contents/generations/tasks/cgt-existing":
pollPath = r.URL.Path
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "cgt-existing",
"status": "succeeded",
"created_at": 789,
"content": map[string]any{"video_url": "https://example.com/resumed.mp4"},
})
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
}))
defer server.Close()
response, err := (VolcesClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "videos.generations",
ModelType: "video_generate",
Model: "豆包Seedance-2.0",
Body: map[string]any{"prompt": "resume polling", "pollIntervalMs": 100, "pollTimeoutSeconds": 1},
RemoteTaskID: "cgt-existing",
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "豆包Seedance-2.0",
Credentials: map[string]any{"apiKey": "volces-key"},
},
})
if err != nil {
t.Fatalf("resume volces video: %v", err)
}
if submitCalled || pollPath != "/contents/generations/tasks/cgt-existing" {
t.Fatalf("resume should poll existing task only, submit=%v poll=%s", submitCalled, pollPath)
}
data, _ := response.Result["data"].([]any)
item, _ := data[0].(map[string]any)
if response.Result["upstream_task_id"] != "cgt-existing" || item["url"] != "https://example.com/resumed.mp4" {
t.Fatalf("unexpected resumed response: %+v", response.Result)
}
}
func extractText(result map[string]any) string {
choices, _ := result["choices"].([]any)
choice, _ := choices[0].(map[string]any)
message, _ := choice["message"].(map[string]any)
text, _ := message["content"].(string)
return text
}