Files
easyai-ai-gateway/apps/api/internal/httpapi/kling_compat_test.go
easyai 9d4501bc42
ci / verify (pull_request) Successful in 15m32s
feat(kling): 接入O1与3.0 Omni兼容接口
新增中国区可灵 V1 AK/SK Omni 协议与 API 2.0 兼容路径,补齐任务隔离、外部任务幂等、参数校验和 OpenAPI 文档。\n\n验证:O1 与 3.0 Omni 真实 V1 任务成功;Go、前端、依赖审计、迁移及 CI 脚本门禁通过。
2026-07-21 23:47:13 +08:00

138 lines
5.7 KiB
Go

package httpapi
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestKlingV1O1CompatibilityBody(t *testing.T) {
body, externalID, err := klingCompatTaskBody("v1", klingO1Model, map[string]any{
"prompt": "一只纸鹤飞过湖面",
"duration": json.Number("10"),
"aspect_ratio": "16:9",
"sound": "on",
"external_task_id": "client-o1-1",
"watermark_info": map[string]any{"enabled": true},
})
if err != nil {
t.Fatalf("build O1 compatibility body: %v", err)
}
if externalID != "client-o1-1" || body["model"] != klingO1Model || body["modelType"] != "omni_video" {
t.Fatalf("unexpected identity fields: %#v", body)
}
if body["mode"] != "pro" || body["resolution"] != "1080p" || body["audio"] != true {
t.Fatalf("unexpected V1 defaults: %#v", body)
}
content := mapListFromRequest(body["content"])
if len(content) != 1 || content[0]["type"] != "text" || content[0]["text"] != "一只纸鹤飞过湖面" {
t.Fatalf("unexpected canonical content: %#v", content)
}
}
func TestKlingV1V3OmniCompatibilityBody(t *testing.T) {
body, _, err := klingCompatTaskBody("v1", klingV3OmniModel, map[string]any{
"multi_shot": true,
"shot_type": "customize",
"mode": "4k",
"multi_prompt": []any{
map[string]any{"index": json.Number("1"), "prompt": "推近人物", "duration": json.Number("7")},
map[string]any{"index": json.Number("2"), "prompt": "切到城市远景", "duration": json.Number("8")},
},
"image_list": []any{
map[string]any{"image_url": "https://example.com/first.png", "type": "first_frame"},
},
})
if err != nil {
t.Fatalf("build 3.0 Omni compatibility body: %v", err)
}
if body["resolution"] != "2160p" {
t.Fatalf("4k mode was not normalized: %#v", body)
}
content := mapListFromRequest(body["content"])
if len(content) != 1 || content[0]["role"] != "first_frame" {
t.Fatalf("image input was not normalized: %#v", content)
}
}
func TestKlingV2CompatibilityBody(t *testing.T) {
body, externalID, err := klingCompatTaskBody("v2", klingV3OmniModel, map[string]any{
"contents": []any{
map[string]any{"type": "prompt", "text": "让角色向镜头挥手"},
map[string]any{"type": "first_frame", "url": "https://example.com/first.png"},
map[string]any{"type": "element", "id": json.Number("42")},
},
"settings": map[string]any{
"resolution": "1080p",
"duration": json.Number("15"),
"aspect_ratio": "9:16",
"audio": "native",
},
"options": map[string]any{
"external_task_id": "client-v2-1",
"callback_url": "https://example.com/callback",
},
})
if err != nil {
t.Fatalf("build V2 compatibility body: %v", err)
}
if externalID != "client-v2-1" || body["mode"] != "pro" || body["audio"] != true {
t.Fatalf("unexpected V2 settings: %#v", body)
}
if len(mapListFromRequest(body["image_list"])) != 1 || len(mapListFromRequest(body["element_list"])) != 1 {
t.Fatalf("unexpected V2 references: %#v", body)
}
}
func TestKlingCompatibilityValidation(t *testing.T) {
tests := []struct {
name string
model string
body map[string]any
}{
{name: "O1 duration", model: klingO1Model, body: map[string]any{"prompt": "test", "duration": 11}},
{name: "O1 text-only flexible duration", model: klingO1Model, body: map[string]any{"prompt": "test", "duration": 3}},
{name: "O1 4k", model: klingO1Model, body: map[string]any{"prompt": "test", "duration": 5, "mode": "4k"}},
{name: "O1 multi-shot", model: klingO1Model, body: map[string]any{"multi_shot": true, "multi_prompt": []any{map[string]any{"prompt": "test", "duration": 3}}}},
{name: "custom multi-shot without prompts", model: klingV3OmniModel, body: map[string]any{"multi_shot": true, "shot_type": "customize"}},
{name: "intelligence multi-shot without prompt", model: klingV3OmniModel, body: map[string]any{"multi_shot": true, "shot_type": "intelligence"}},
{name: "video with native audio", model: klingV3OmniModel, body: map[string]any{"prompt": "test", "sound": "on", "video_list": []any{map[string]any{"video_url": "https://example.com/input.mp4"}}}},
{name: "video duration too long", model: klingV3OmniModel, body: map[string]any{"prompt": "test", "duration": 15, "video_list": []any{map[string]any{"video_url": "https://example.com/input.mp4"}}}},
{name: "too many references", model: klingV3OmniModel, body: map[string]any{"prompt": "test", "image_list": []any{
map[string]any{}, map[string]any{}, map[string]any{}, map[string]any{}, map[string]any{}, map[string]any{}, map[string]any{}, map[string]any{},
}}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, _, err := klingCompatTaskBody("v1", test.model, test.body); err == nil {
t.Fatal("expected validation error")
}
})
}
if _, _, err := klingCompatTaskBody("v1", klingV3OmniModel, map[string]any{"prompt": "test", "duration": 15}); err != nil {
t.Fatalf("3.0 Omni should allow a 15-second duration: %v", err)
}
}
func TestDecodeKlingJSONRejectsTrailingData(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "/kling/v1/videos/omni-video", strings.NewReader(`{"prompt":"ok"} trailing`))
var body map[string]any
if err := decodeKlingJSON(request, &body); err == nil {
t.Fatal("expected trailing JSON error")
}
}
func TestWriteKlingCompatErrorUsesOfficialNumericEnvelope(t *testing.T) {
recorder := httptest.NewRecorder()
writeKlingCompatError(recorder, http.StatusBadRequest, "bad request", "invalid_parameter")
var body map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatalf("decode error response: %v", err)
}
if body["code"] != float64(1001) || body["error"] != "invalid_parameter" {
t.Fatalf("unexpected error envelope: %#v", body)
}
}