Files
ttstd-infinite-canvas/backend/internal/app/logical_models_test.go
strive_qin 074612bc1f fix(app): 前台图片模型 - 分辨率档位与各供应线路取并集,修掉只显示单档
capabilitySpecWithRoutePresets 原先只在「前台自身没有任何尺寸预设」时才与线路合并。但前台快照的预设往往来自单条线路(后续新增的供应线路还没同步进来),条件不成立后,其它线路的 2K/4K 预设永远补不进来,前台只显示 1K。

现在无论自身是否已有预设都与各线路取并集(mergeCapabilityImageSize 已包含前台自身的预设,并集不会丢档)。

线上数据可复现:image-2.0-1k 的前台规格 imageSize.presets 全部是 1k,而它四条线路分别是 none/none/1k/2k+4k。补了一条回归测试,反向验证确认旧逻辑下该测试精确报出 tier 2k missing。
2026-09-16 19:49:57 +08:00

264 lines
10 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 app
import (
"strings"
"testing"
"infinite-canvas/backend/internal/model"
)
func TestCapabilitySpecWithRoutePresetsRestoresImageSize(t *testing.T) {
product := CapabilitySpec{Version: 1, Capability: "image", Options: map[string]OptionConstraint{
"size": {Values: []any{"16:9"}},
}}
route := CapabilitySpec{
Version: 1,
Capability: "image",
Options: map[string]OptionConstraint{"size": {Values: []any{"16:9"}}},
ImageSize: &CapabilityImageSize{
Parameter: "aspect_ratio",
Presets: []CapabilityImageSizePreset{{Size: "3840x2160", Tier: "4k", Ratio: "16:9", Width: 3840, Height: 2160}},
},
}
got := capabilitySpecWithRoutePresets(product, []CapabilitySpec{route})
if got.ImageSize == nil || got.ImageSize.Parameter != "aspect_ratio" || len(got.ImageSize.Presets) != 1 || got.ImageSize.Presets[0].Tier != "4k" {
t.Fatalf("restored imageSize = %#v", got.ImageSize)
}
}
func TestCapabilitySpecWithRoutePresetsUnionsTiersAcrossRoutes(t *testing.T) {
// 前台快照只带了单条线路的 1K 预设,其它线路提供 2K/4K:必须取并集,不能压成单档。
preset := func(size string, tier string, width int, height int) CapabilityImageSizePreset {
return CapabilityImageSizePreset{Size: size, Tier: tier, Ratio: "16:9", Width: width, Height: height}
}
product := CapabilitySpec{
Version: 1,
Capability: "image",
Options: map[string]OptionConstraint{"size": {Values: []any{"16:9"}}},
ImageSize: &CapabilityImageSize{
Parameter: "aspect_ratio",
Presets: []CapabilityImageSizePreset{preset("1824x1024", "1k", 1824, 1024)},
},
}
routes := []CapabilitySpec{
{Version: 1, Capability: "image", ImageSize: &CapabilityImageSize{Parameter: "aspect_ratio", Presets: []CapabilityImageSizePreset{preset("2752x1536", "2k", 2752, 1536)}}},
{Version: 1, Capability: "image", ImageSize: &CapabilityImageSize{Parameter: "aspect_ratio", Presets: []CapabilityImageSizePreset{preset("3840x2160", "4k", 3840, 2160)}}},
}
got := capabilitySpecWithRoutePresets(product, routes)
if got.ImageSize == nil {
t.Fatal("imageSize = nil")
}
tiers := make(map[string]bool, len(got.ImageSize.Presets))
for _, item := range got.ImageSize.Presets {
tiers[item.Tier] = true
}
for _, tier := range []string{"1k", "2k", "4k"} {
if !tiers[tier] {
t.Fatalf("tier %s missing: %#v", tier, got.ImageSize.Presets)
}
}
}
func TestMatchCapabilityRoutesTextImagesOnlyToDeclaredProfiles(t *testing.T) {
plainText := CapabilitySpec{
Version: 1,
Capability: "text",
Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 0}},
}
visionText := CapabilitySpec{
Version: 1,
Capability: "text",
Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 8}},
}
intent := ModelRequestIntent{Capability: "text", Inputs: map[string]int{"image": 2}}
if match := MatchCapability(plainText, intent); match.Matched {
t.Fatalf("plain text profile unexpectedly matched image input: %#v", match)
}
if match := MatchCapability(visionText, intent); !match.Matched {
t.Fatalf("vision text profile did not match image input: %#v", match)
}
}
func TestMatchCapabilityAcceptsWildcardOptionValues(t *testing.T) {
spec := CapabilitySpec{
Version: 1,
Capability: "image",
Options: map[string]OptionConstraint{"size": {Values: []any{"*"}}},
}
intent := ModelRequestIntent{Capability: "image", Options: map[string]any{"size": "1024x1024"}}
if match := MatchCapability(spec, intent); !match.Matched {
t.Fatalf("wildcard option did not match custom value: %#v", match)
}
}
func TestMatchCapabilityAcceptsWildcardAlongsidePresetOptionValues(t *testing.T) {
intent := ModelRequestIntent{Capability: "image", Options: map[string]any{"size": "2:1"}}
spec := CapabilitySpec{
Version: 1,
Capability: "image",
Options: map[string]OptionConstraint{"size": {Values: []any{"1:1", "9:16", "*"}}},
}
match := MatchCapability(spec, intent)
if !match.Matched {
t.Fatalf("wildcard alongside presets did not match custom value: %#v", match)
}
}
func TestMatchCapabilityTreatsVideoResolutionSuffixAsEquivalent(t *testing.T) {
spec := CapabilitySpec{
Version: 1,
Capability: "video",
Options: map[string]OptionConstraint{"vquality": {Values: []any{"720p"}}},
}
intent := ModelRequestIntent{Capability: "video", Options: map[string]any{"vquality": "720"}}
if match := MatchCapability(spec, intent); !match.Matched {
t.Fatalf("720 and 720p should match the same video resolution: %#v", match)
}
}
func TestVideoResolutionAliasesAcrossProductAndPricedRoutes(t *testing.T) {
product := CapabilitySpec{
Version: 1, Capability: "video",
Options: map[string]OptionConstraint{"vquality": {Values: []any{"768P", "2K"}}},
}
route := capabilitySpecWithPriceTiers(product, model.ChannelModel{
PriceTiers: []model.ChannelModelPriceTier{
{Resolution: "768P", Enabled: true, PriceConfigured: true},
{Resolution: "2K", Enabled: true, PriceConfigured: true},
},
})
if err := validateProductSpecWithinRoutes(product, []CapabilitySpec{route}); err != nil {
t.Fatalf("2K product rejected against priced route: %v", err)
}
for _, resolution := range []string{"2K", "2k", "1440P", "1440"} {
intent := ModelRequestIntent{Capability: "video", Options: map[string]any{
"vquality": normalizeModelRequestOption("vquality", resolution),
}}
for name, spec := range map[string]CapabilitySpec{"product": product, "route": route} {
if match := MatchCapability(spec, intent); !match.Matched {
t.Errorf("%s rejected %s: %#v", name, resolution, match)
}
}
}
unsupported := CapabilitySpec{Version: 1, Capability: "video", Options: map[string]OptionConstraint{
"vquality": {Values: []any{"4K"}},
}}
if err := validateProductSpecWithinRoutes(unsupported, []CapabilitySpec{route}); err == nil {
t.Fatal("2K route accepted unsupported 4K product")
}
if capabilityOptionValuesEqual("quality", "2K", "1440p") {
t.Fatal("video aliases must not affect image quality")
}
}
func TestValidateProductSpecWithinRoutesRejectsUnsupportedCapabilityValue(t *testing.T) {
routes := []CapabilitySpec{
{
Version: 1,
Capability: "video",
Operations: []string{"text_to_video"},
Options: map[string]OptionConstraint{
"vquality": {Values: []any{"720p"}},
},
},
{
Version: 1,
Capability: "video",
Operations: []string{"image_to_video"},
Options: map[string]OptionConstraint{
"vquality": {Values: []any{"1080p"}},
},
},
}
product := CapabilitySpec{
Version: 1,
Capability: "video",
Operations: []string{"text_to_video", "image_to_video"},
Options: map[string]OptionConstraint{
"vquality": {Values: []any{"720p", "4k"}},
},
}
err := validateProductSpecWithinRoutes(product, routes)
if err == nil || !strings.Contains(err.Error(), "vquality") {
t.Fatalf("validateProductSpecWithinRoutes() error = %v", err)
}
}
func TestValidateProductSpecWithinRoutesPreservesInputRangeCoverage(t *testing.T) {
routes := []CapabilitySpec{
{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 4}}},
{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 5, Max: 9}}},
}
covered := CapabilitySpec{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 9}}}
if err := validateProductSpecWithinRoutes(covered, routes); err != nil {
t.Fatalf("covered input range rejected: %v", err)
}
routes[1].Inputs["image"] = InputConstraint{Min: 6, Max: 9}
if err := validateProductSpecWithinRoutes(covered, routes); err == nil {
t.Fatal("input range with an uncovered count was accepted")
}
}
func TestLogicalModelConfigurationErrorAfterRouteCoverageShrinks(t *testing.T) {
product := CapabilitySpec{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 9}}}
complete := []CapabilitySpec{
{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 4}}},
{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 5, Max: 9}}},
}
if message := logicalModelConfigurationError(product, complete); message != "" {
t.Fatalf("complete route coverage reported an error: %s", message)
}
if message := logicalModelConfigurationError(product, complete[:1]); !strings.Contains(message, "无法完整覆盖") {
t.Fatalf("shrunk route coverage error = %q", message)
}
}
func TestLogicalModelAvailabilityErrorRequiresSettlementReadyCoverage(t *testing.T) {
product := CapabilitySpec{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 9}}}
structural := []CapabilitySpec{
{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 0, Max: 4}}},
{Version: 1, Capability: "image", Inputs: map[string]InputConstraint{"image": {Min: 5, Max: 9}}},
}
if message := logicalModelAvailabilityError("channel", product, structural, nil); !strings.Contains(message, "可结算价格") {
t.Fatalf("missing settlement route error = %q", message)
}
if message := logicalModelAvailabilityError("channel", product, structural, structural[:1]); !strings.Contains(message, "部分创作端能力") {
t.Fatalf("partial settlement coverage error = %q", message)
}
if message := logicalModelAvailabilityError("channel", product, structural, structural); message != "" {
t.Fatalf("complete settlement coverage error = %q", message)
}
if message := logicalModelAvailabilityError("unified", product, structural, nil); message != "" {
t.Fatalf("unified pricing unexpectedly required channel prices: %q", message)
}
}
func TestSupportsLogicalModelTokenBillingForArkVideoRoutes(t *testing.T) {
if !supportsLogicalModelTokenBilling("text", nil) {
t.Fatal("text logical models should support Token billing")
}
if !supportsLogicalModelTokenBilling("video", []model.ChannelInterfaceType{model.ChannelInterfaceVolcengineArkVideo}) {
t.Fatal("Ark video-only routes should support Token billing")
}
if supportsLogicalModelTokenBilling("video", nil) {
t.Fatal("video logical models without an enabled route must not support Token billing")
}
if supportsLogicalModelTokenBilling("video", []model.ChannelInterfaceType{model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceNewAPIVideo}) {
t.Fatal("mixed video protocols must not support Token billing")
}
}
func TestChannelModelCapabilitySpecRequiresExplicitImageCapability(t *testing.T) {
channelModel := model.ChannelModel{Capability: "image", CapabilityConfigJSON: ""}
_, err := channelModelCapabilitySpec(channelModel)
if err == nil || !strings.Contains(err.Error(), "渠道图片模型尚未配置能力参数") {
t.Fatalf("channelModelCapabilitySpec() error = %v", err)
}
}