268 lines
11 KiB
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")
|
|
}
|
|
})
|
|
}
|
|
}
|