Files
ttstd-infinite-canvas/backend/internal/app/channel_model_pricing_test.go

268 lines
11 KiB
Go

package app
import (
"errors"
"testing"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
"gorm.io/gorm"
)
func seedRepricingModels(t *testing.T) (*Service, *gorm.DB, *model.User) {
t.Helper()
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
if err := db.Create(&model.ModelChannel{ID: "channel", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true}).Error; err != nil {
t.Fatal(err)
}
for _, id := range []string{"a", "b"} {
item := model.ChannelModel{ID: id, ChannelID: "channel", ModelKey: id, DisplayName: id, Capability: "text", Protocol: model.ChannelInterfaceChatCompletion, BillingMode: "fixed_request", Enabled: true, PriceConfigured: true, PriceVersion: 1, UnitPriceMicrocredits: 9}
tier := model.ChannelModelPriceTier{ID: "tier-" + id, ChannelModelID: id, SelectorKey: "{}", SelectorJSON: "{}", Resolution: "*", BillingMode: "fixed_request", Enabled: true, PriceConfigured: true, PriceVersion: 1, UnitPriceMicrocredits: 9, CostPricing: model.CreditCostPricing{Configured: true, UnitPriceMicrocredits: 1_000_000}}
if err := db.Create(&item).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&tier).Error; err != nil {
t.Fatal(err)
}
}
return svc, db, admin
}
func repricingRequest(id string, price int64) ChannelModelRepriceRequest {
return ChannelModelRepriceRequest{ModelID: id, PriceVersion: 1, PriceTiers: []ChannelModelTierRepriceRequest{{ID: "tier-" + id, PriceVersion: 1, Prices: map[string]*int64{"unitPriceMicrocredits": &price}}}}
}
func TestRepriceAdminChannelModelsPreservesFinalManualPrices(t *testing.T) {
svc, db, admin := seedRepricingModels(t)
disabled := model.ChannelModelPriceTier{ID: "disabled", ChannelModelID: "a", SelectorKey: `{"quality":"high"}`, Enabled: false, UnitPriceMicrocredits: 99, PriceVersion: 7}
if err := db.Create(&disabled).Error; err != nil {
t.Fatal(err)
}
if err := db.Model(&model.ChannelModel{}).Where("id = ?", "b").Update("enabled", false).Error; err != nil {
t.Fatal(err)
}
requests := []ChannelModelRepriceRequest{repricingRequest("a", 1_250_000), repricingRequest("b", 1_370_000)}
updated, err := svc.RepriceAdminChannelModels(admin, "channel", requests)
if err != nil || updated != 2 {
t.Fatalf("updated=%d err=%v", updated, err)
}
for _, request := range requests {
item, err := svc.repo.ChannelModelByID("channel", request.ModelID)
if err != nil {
t.Fatal(err)
}
want := *request.PriceTiers[0].Prices["unitPriceMicrocredits"]
if item.UnitPriceMicrocredits != want || item.PriceVersion != 2 || item.Enabled != (item.ID == "a") {
t.Fatalf("model=%+v", item)
}
for _, tier := range item.PriceTiers {
if tier.ID == "disabled" {
if tier.UnitPriceMicrocredits != 99 || tier.PriceVersion != 7 || tier.Enabled {
t.Fatalf("disabled=%+v", tier)
}
} else if tier.UnitPriceMicrocredits != want || tier.PriceVersion != 2 || tier.CostPricing.UnitPriceMicrocredits != 1_000_000 {
t.Fatalf("tier=%+v", tier)
}
}
}
if _, err := svc.RepriceAdminChannelModels(admin, "channel", requests); err == nil {
t.Fatal("stale preview accepted")
}
}
func TestRepriceAdminChannelModelsMultipleSpecifications(t *testing.T) {
svc, db, admin := seedRepricingModels(t)
if err := db.Model(&model.ChannelModel{}).Where("id = ?", "a").Updates(map[string]any{
"capability": "image", "protocol": model.ChannelInterfaceOpenAIImage,
}).Error; err != nil {
t.Fatal(err)
}
request := repricingRequest("a", 4_000)
for _, spec := range []struct {
id string
price int64
}{{"2K", 5_500}, {"4K", 6_000}} {
tier := model.ChannelModelPriceTier{ID: spec.id, ChannelModelID: "a", SelectorKey: `{"vquality":"` + spec.id + `"}`,
SelectorJSON: `{"vquality":"` + spec.id + `"}`, Resolution: spec.id, BillingMode: "fixed_request",
Enabled: true, PriceConfigured: true, PriceVersion: 1, UnitPriceMicrocredits: 9,
CostPricing: model.CreditCostPricing{Configured: true, UnitPriceMicrocredits: 3_000}}
if err := db.Create(&tier).Error; err != nil {
t.Fatal(err)
}
request.PriceTiers = append(request.PriceTiers, ChannelModelTierRepriceRequest{ID: spec.id, PriceVersion: 1,
Prices: map[string]*int64{"unitPriceMicrocredits": &spec.price}})
}
if updated, err := svc.RepriceAdminChannelModels(admin, "channel", []ChannelModelRepriceRequest{request}); err != nil || updated != 1 {
t.Fatalf("updated=%d err=%v", updated, err)
}
for _, input := range request.PriceTiers {
var tier model.ChannelModelPriceTier
if err := db.First(&tier, "id = ?", input.ID).Error; err != nil {
t.Fatal(err)
}
if tier.UnitPriceMicrocredits != *input.Prices["unitPriceMicrocredits"] || tier.PriceVersion != 2 {
t.Fatalf("specification price was not saved independently: %+v", tier)
}
}
}
func TestRepriceAdminChannelModelsRejectsWholeBatch(t *testing.T) {
for _, tt := range []struct {
name string
edit func([]ChannelModelRepriceRequest) []ChannelModelRepriceRequest
mutate func(*gorm.DB) error
nonAdmin bool
}{
{name: "empty", edit: func(_ []ChannelModelRepriceRequest) []ChannelModelRepriceRequest { return nil }},
{name: "too many", edit: func(_ []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
return make([]ChannelModelRepriceRequest, 101)
}},
{name: "duplicate model", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest { return append(r, r[0]) }},
{name: "empty ID", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest { r[1].ModelID = ""; return r }},
{name: "stale model", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest { r[1].PriceVersion = 2; return r }},
{name: "stale tier", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1].PriceTiers[0].PriceVersion = 2
return r
}},
{name: "missing tier", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest { r[1].PriceTiers = nil; return r }},
{name: "wrong tier owner", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1].PriceTiers[0].ID = "tier-a"
return r
}},
{name: "duplicate tier", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1].PriceTiers = append(r[1].PriceTiers, r[1].PriceTiers[0])
return r
}},
{name: "negative price", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1] = repricingRequest("b", -1)
return r
}},
{name: "missing price", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1].PriceTiers[0].Prices = nil
return r
}},
{name: "null price", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1].PriceTiers[0].Prices["unitPriceMicrocredits"] = nil
return r
}},
{name: "unknown field", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1].PriceTiers[0].Prices["enabled"] = nil
return r
}},
{name: "unsafe price", edit: func(r []ChannelModelRepriceRequest) []ChannelModelRepriceRequest {
r[1] = repricingRequest("b", 9_007_199_254_740_992)
return r
}},
{name: "missing cost", mutate: func(db *gorm.DB) error {
return db.Model(&model.ChannelModelPriceTier{}).Where("id = ?", "tier-b").Update("cost_configured", false).Error
}},
{name: "disabled tier", mutate: func(db *gorm.DB) error {
return db.Model(&model.ChannelModelPriceTier{}).Where("id = ?", "tier-b").Update("enabled", false).Error
}},
{name: "cross channel", mutate: func(db *gorm.DB) error {
return db.Model(&model.ChannelModel{}).Where("id = ?", "b").Update("channel_id", "other").Error
}},
{name: "deleted", mutate: func(db *gorm.DB) error { return db.Delete(&model.ChannelModel{}, "id = ?", "b").Error }},
{name: "not admin", nonAdmin: true},
} {
t.Run(tt.name, func(t *testing.T) {
svc, db, actor := seedRepricingModels(t)
requests := []ChannelModelRepriceRequest{repricingRequest("a", 1_250_000), repricingRequest("b", 1_370_000)}
if tt.edit != nil {
requests = tt.edit(requests)
}
if tt.mutate != nil {
if err := tt.mutate(db); err != nil {
t.Fatal(err)
}
}
if tt.nonAdmin {
actor = &model.User{ID: "user"}
}
if count, err := svc.RepriceAdminChannelModels(actor, "channel", requests); err == nil || count != 0 {
t.Fatalf("count=%d err=%v", count, err)
}
item, err := svc.repo.ChannelModelByID("channel", "a")
if err != nil {
t.Fatal(err)
}
if item.PriceVersion != 1 || item.PriceTiers[0].UnitPriceMicrocredits != 9 {
t.Fatal("partial save")
}
})
}
}
func TestRepriceTierFieldValidation(t *testing.T) {
for _, capability := range []string{"text", "video"} {
output, input, zero := int64(1_370_000), int64(500_000), int64(0)
tier := model.ChannelModelPriceTier{ID: "t", BillingMode: "token", PriceConfigured: true, CostPricing: model.CreditCostPricing{Configured: true}}
prices := map[string]*int64{"outputTokenPriceMicrocredits": &output}
protocol := model.ChannelInterfaceVolcengineArkVideo
if capability == "text" {
protocol = model.ChannelInterfaceChatCompletion
prices["inputTokenPriceMicrocredits"] = &input
prices["cachedTokenPriceMicrocredits"] = &zero
}
if err := applyChannelModelTierPrices(&tier, capability, protocol, prices); err != nil {
t.Fatal(err)
}
if tier.OutputTokenPriceMicrocredits != output || tier.CachedTokenPriceMicrocredits != 0 {
t.Fatalf("tier=%+v", tier)
}
output = 1_000_000_000_001
if err := applyChannelModelTierPrices(&tier, capability, protocol, prices); err == nil {
t.Fatal("token price limit missing")
}
}
}
func TestNormalizeChannelModelTierSelectorAcceptsResolutionTypo(t *testing.T) {
selector, resolution, videoSeconds, err := normalizeChannelModelTierSelector("image", ChannelModelPriceTierRequest{
Selector: map[string]string{"resoluton": "2K"},
})
if err != nil {
t.Fatal(err)
}
if selector["resolution"] != "2k" || selector["resoluton"] != "" || resolution != "*" || videoSeconds != 0 {
t.Fatalf("selector=%v resolution=%q videoSeconds=%d", selector, resolution, videoSeconds)
}
}
for _, target := range []string{"model", "tier"} {
t.Run(target, func(t *testing.T) {
svc, db, _ := seedRepricingModels(t)
items := make([]model.ChannelModel, 0, 2)
for _, id := range []string{"a", "b"} {
item, err := svc.repo.ChannelModelByID("channel", id)
if err != nil {
t.Fatal(err)
}
item.UnitPriceMicrocredits = 123
item.PriceTiers[0].UnitPriceMicrocredits = 123
items = append(items, *item)
}
var err error
if target == "model" {
err = db.Model(&model.ChannelModel{}).Where("id = ?", "b").Update("price_version", 2).Error
} else {
err = db.Model(&model.ChannelModelPriceTier{}).Where("id = ?", "tier-b").Update("price_version", 2).Error
}
if err != nil {
t.Fatal(err)
}
if err := svc.repo.UpdateChannelModelSalePrices("channel", items); !errors.Is(err, repository.ErrChannelModelPriceConflict) {
t.Fatalf("error=%v", err)
}
item, err := svc.repo.ChannelModelByID("channel", "a")
if err != nil {
t.Fatal(err)
}
if item.UnitPriceMicrocredits != 9 || item.PriceVersion != 1 || item.PriceTiers[0].UnitPriceMicrocredits != 9 || item.PriceTiers[0].PriceVersion != 1 {
t.Fatal("transaction not rolled back")
}
})
}
}