Files
ttstd-infinite-canvas/backend/internal/app/logical_models_test.go
ddCat d7388fa9a9 fix(app,canvas): 图片档位并集忽略停用线路,重生结果不再继承旧素材绑定
前台分辨率并集只收启用且权重大于 0 的供应线路,避免停用 4K 线路后仍出现不可路由档位。生成回写新媒体时清除旧 assetId,堵住原地重生后「旧素材 + 新资源」配对。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 00:08:35 +08:00

295 lines
12 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 TestEnabledLogicalRouteSpecsOmitsDisabledAndZeroWeightTiers(t *testing.T) {
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",
ImageSize: &CapabilityImageSize{Parameter: "aspect_ratio", Presets: []CapabilityImageSizePreset{preset("1824x1024", "1k", 1824, 1024)}},
}
routes := []cachedLogicalRoute{
{Route: model.LogicalModelRoute{Enabled: true, Weight: 1}, CapabilitySpec: CapabilitySpec{ImageSize: &CapabilityImageSize{Presets: []CapabilityImageSizePreset{preset("2752x1536", "2k", 2752, 1536)}}}},
{Route: model.LogicalModelRoute{Enabled: false, Weight: 1}, CapabilitySpec: CapabilitySpec{ImageSize: &CapabilityImageSize{Presets: []CapabilityImageSizePreset{preset("3840x2160", "4k", 3840, 2160)}}}},
{Route: model.LogicalModelRoute{Enabled: true, Weight: 0}, CapabilitySpec: CapabilitySpec{ImageSize: &CapabilityImageSize{Presets: []CapabilityImageSizePreset{preset("4096x2304", "4k", 4096, 2304)}}}},
}
got := capabilitySpecWithRoutePresets(product, enabledLogicalRouteSpecs(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
}
if !tiers["1k"] || !tiers["2k"] {
t.Fatalf("expected 1k and 2k from enabled routes: %#v", got.ImageSize.Presets)
}
if tiers["4k"] {
t.Fatalf("disabled and zero-weight 4k routes must not appear in public presets: %#v", 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)
}
}