Files
ttstd-infinite-canvas/backend/internal/protocol/agnes_image_test.go

163 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package protocol
import (
"context"
"testing"
)
// Agnes 图像端点是同步的,文生图与图生图共用 POST /v1/images/generations。
// 官方参数表只承认 model、prompt、size(必填)、ratio、image、return_base64 与
// extra_body:顶层 response_format 是官方明确列出的错误写法,output_format 会被
// 直接拒绝,n 也不在参数表内。因此这里逐字段固定线协议,防止退回 OpenAI 兼容字段。
func TestAgnesImageSendsOnlyDocumentedFields(t *testing.T) {
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
if adapter.Metadata().RequiresPublicMediaURLs {
t.Fatal("Agnes 图像支持 Data URI 参考图,不应强制要求公共媒体 URL")
}
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "agnes-image-2.1-flash", Prompt: "海边写真的亚洲女模特", AspectRatio: "3:4", Quality: "2k", ImageCount: 4,
}})
if err != nil {
t.Fatal(err)
}
if create.Method != "POST" || create.Path != "/v1/images/generations" {
t.Fatalf("create = %#v", create)
}
// 原生端点在域名根下,originPath 让请求路径替换 Base URL 的路径段,
// 渠道沿用 .../v1 也能命中正确地址。
if !create.OriginPath {
t.Fatal("create 必须声明 originPath")
}
body := manifestTestBody(t, create)
if body["model"] != "agnes-image-2.1-flash" || body["prompt"] != "海边写真的亚洲女模特" {
t.Fatalf("body = %#v", body)
}
if body["size"] != "2K" {
t.Fatalf("size = %#v, 期望按质量档位发送 2K", body["size"])
}
if body["ratio"] != "3:4" {
t.Fatalf("ratio = %#v, 期望独立的 ratio 字段", body["ratio"])
}
// 官方参数表之外的字段会被上游按“不支持该字段”拒绝,逐个钉死。
for _, forbidden := range []string{"n", "output_format", "quality", "background", "response_format", "image", "mask", "style", "user"} {
if _, present := body[forbidden]; present {
t.Fatalf("body = %#v, 官方参数表没有 %s 字段,不能发送", body, forbidden)
}
}
extra, ok := body["extra_body"].(map[string]any)
if !ok {
t.Fatalf("extra_body = %#v", body["extra_body"])
}
if extra["response_format"] != "url" {
t.Fatalf("extra_body.response_format = %#v, 输出格式只能声明在 extra_body 内", extra["response_format"])
}
if _, present := extra["image"]; present {
t.Fatalf("extra_body = %#v, 文生图不应发送 image", extra)
}
}
func TestAgnesImagePutsReferencesInExtraBody(t *testing.T) {
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "agnes-image-2.1-flash", Prompt: "换装并保留原始构图", AspectRatio: "3:4", Quality: "1k",
Images: []MediaReference{
{URL: "https://cdn.example/anchor.png", Role: "edit_source", Order: 0},
{DataURL: "data:image/png;base64,QUJD", Role: "edit_source", Order: 1},
},
}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, create)
if _, present := body["image"]; present {
t.Fatalf("body = %#v, 参考图必须放在 extra_body.image,而不是顶层 image", body)
}
extra, _ := body["extra_body"].(map[string]any)
images, ok := extra["image"].([]any)
if !ok || len(images) != 2 {
t.Fatalf("extra_body.image = %#v, 期望两张参考图", extra["image"])
}
if images[0] != "https://cdn.example/anchor.png" {
t.Fatalf("第一张 = %#v, order 决定顺序", images[0])
}
// 有 Data URL 时优先 Base64:Agnes 明确支持 Data URI,本地开发无法提供公共图片地址。
if images[1] != "data:image/png;base64,QUJD" {
t.Fatalf("第二张 = %#v, 有 dataUrl 时必须优先 Base64", images[1])
}
if body["size"] != "1K" {
t.Fatalf("size = %#v, 图生图同样必须携带必填的 size", body["size"])
}
}
func TestAgnesImageSizeFollowsPixelOrTier(t *testing.T) {
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
tests := []struct {
name string
aspectRatio string
quality string
wantSize string
wantRatio string
}{
{name: "比例落到分辨率档位", aspectRatio: "16:9", quality: "4k", wantSize: "4K", wantRatio: "16:9"},
{name: "像素尺寸原样透传", aspectRatio: "1024x768", quality: "2k", wantSize: "1024x768"},
{name: "auto 也必须落到档位", aspectRatio: "auto", quality: "1k", wantSize: "1K"},
{name: "空比例回落到默认档位", aspectRatio: "", quality: "", wantSize: "1K"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "agnes-image-2.1-flash", Prompt: "circle", AspectRatio: test.aspectRatio, Quality: test.quality,
}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, create)
if body["size"] != test.wantSize {
t.Fatalf("size = %#v, 期望 %q", body["size"], test.wantSize)
}
gotRatio, present := body["ratio"]
if test.wantRatio == "" {
if present {
t.Fatalf("ratio = %#v, 非比例输入不得发送 ratio", gotRatio)
}
return
}
if gotRatio != test.wantRatio {
t.Fatalf("ratio = %#v, 期望 %q", gotRatio, test.wantRatio)
}
})
}
}
func TestAgnesImageRejectsUnsupportedRatio(t *testing.T) {
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
// 9:21 / 4:5 等比例不在官方比例表内,静默回落到 1:1 会让用户拿到与选择不符的画幅。
if _, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "agnes-image-2.1-flash", Prompt: "x", AspectRatio: "9:21",
}}); err == nil {
t.Fatal("9:21 不在官方比例表内,必须在校验阶段拒绝")
}
}
func TestAgnesImageReadsUrlAndBase64Output(t *testing.T) {
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
result, err := adapter.ParseCreate(context.Background(), []byte(`{"created":1780000000,"data":[{"url":"https://storage.googleapis.com/agnes-aigc/x.png","b64_json":null}]}`))
if err != nil {
t.Fatal(err)
}
if result.Result == nil || len(result.Result.Images) != 1 || result.Result.Images[0].URL != "https://storage.googleapis.com/agnes-aigc/x.png" {
t.Fatalf("images = %#v", result.Result)
}
inline, err := adapter.ParseCreate(context.Background(), []byte(`{"data":[{"url":null,"b64_json":"aW1hZ2U="}]}`))
if err != nil {
t.Fatal(err)
}
if inline.Result == nil || len(inline.Result.Images) != 1 || inline.Result.Images[0].DataURL != "data:image/png;base64,aW1hZ2U=" {
t.Fatalf("images = %#v", inline.Result)
}
}