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

222 lines
9.6 KiB
Go

package app
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
)
func TestVideoTokenChannelQuoteAgreesWithTaskBilling(t *testing.T) {
for _, test := range []struct {
name string
audio any
free bool
wantTier string
wantPrice int64
wantCost int64
}{
{name: "audio default", wantTier: "video-audio", wantPrice: 16_000_000, wantCost: 1_900_800},
{name: "explicit audio", audio: true, wantTier: "video-audio", wantPrice: 16_000_000, wantCost: 1_900_800},
{name: "silent", audio: false, wantTier: "video-silent", wantPrice: 8_000_000, wantCost: 950_400},
{name: "configured free", audio: true, free: true, wantTier: "video-audio"},
} {
t.Run(test.name, func(t *testing.T) {
svc, db, channel, channelModel, _ := newVideoTokenQuoteFixture(t)
if test.free {
if err := db.Model(&model.ChannelModelPriceTier{}).Where("id = ?", test.wantTier).Update("output_token_price_microcredits", 0).Error; err != nil {
t.Fatal(err)
}
}
intent := ModelRequestIntent{Capability: "video", Options: map[string]any{
"vquality": "720p", "size": "16:9", "videoSeconds": 5,
}}
if test.audio != nil {
intent.Options["videoGenerateAudio"] = test.audio
}
quote, err := svc.QuoteChannelModel(ChannelModelQuoteRequest{ChannelID: channel.ID, ModelKey: channelModel.ModelKey, Intent: intent})
if err != nil {
t.Fatalf("QuoteChannelModel() error = %v", err)
}
assertVideoTokenQuote(t, quote, test.wantCost)
assertVideoQuoteHasNoFinancialWrites(t, db)
input := quoteInput(intent, channelModel.ModelKey)
input["config"].(map[string]any)["channelId"] = channel.ID
resolved, err := svc.resolveSystemChannelModelSelection(input, "canvas_video", "")
if err != nil {
t.Fatalf("resolveSystemChannelModelSelection() error = %v", err)
}
order, err := svc.taskBillingOrder("quote-user", &model.Task{ID: "video-task", Type: "canvas_video"}, resolved)
if err != nil {
t.Fatalf("taskBillingOrder() error = %v", err)
}
if order == nil || order.PriceTierID != test.wantTier || order.OutputTokenPriceMicrocredits != test.wantPrice || order.InputTokenPriceMicrocredits != 0 || order.CachedTokenPriceMicrocredits != 0 {
t.Fatalf("wrong video price tier: %#v", order)
}
if order.AmountMicrocredits != quote.AmountMicrocredits || order.ReservedAmountMicrocredits != quote.AmountMicrocredits || order.Quantity != quote.Quantity {
t.Fatalf("quote and task reservation disagree: quote=%#v order=%#v", quote, order)
}
assertVideoQuoteHasNoFinancialWrites(t, db)
})
}
}
func TestVideoTokenLogicalQuoteAgreesWithSystemChannel(t *testing.T) {
for _, policy := range []string{"channel", "unified"} {
t.Run(policy, func(t *testing.T) {
svc, db, channel, channelModel, profile := newVideoTokenQuoteFixture(t)
spec, err := CapabilitySpecFromModelCapabilityConfig(profile, "video")
if err != nil {
t.Fatal(err)
}
specJSON, err := json.Marshal(spec)
if err != nil {
t.Fatal(err)
}
logical := model.LogicalModel{
ID: "logical-video", Code: "seedance-video", Capability: "video", Enabled: true,
ActiveRevisionID: "video-revision", PricePolicy: policy, BillingMode: "token", OutputPriceMicrocredits: 16_000_000,
}
revision := model.LogicalModelRevision{
ID: logical.ActiveRevisionID, LogicalModelID: logical.ID, Version: 1,
CapabilitySpecJSON: string(specJSON),
DefaultOptionsJSON: `{"vquality":"720p","size":"16:9","videoSeconds":5,"videoGenerateAudio":true}`,
}
route := model.LogicalModelRoute{
ID: "video-route", LogicalModelRevisionID: revision.ID, ChannelModelID: channelModel.ID, Enabled: true, Weight: 1,
}
for _, row := range []any{&logical, &revision, &route} {
if err := db.Create(row).Error; err != nil {
t.Fatal(err)
}
}
intent := ModelRequestIntent{Capability: "video"}
logicalQuote, err := svc.QuoteLogicalModel(logical.ID, intent)
if err != nil {
t.Fatalf("QuoteLogicalModel() error = %v", err)
}
assertVideoTokenQuote(t, logicalQuote, 1_900_800)
if logicalQuote.LogicalModelID != logical.ID {
t.Fatalf("logicalModelId = %q, want %q", logicalQuote.LogicalModelID, logical.ID)
}
channelQuote, err := svc.QuoteChannelModel(ChannelModelQuoteRequest{ChannelID: channel.ID, ModelKey: channelModel.ModelKey, Intent: intent})
if err != nil {
t.Fatal(err)
}
if logicalQuote.AmountMicrocredits != channelQuote.AmountMicrocredits || logicalQuote.Quantity != channelQuote.Quantity || !reflect.DeepEqual(logicalQuote.VideoTokenEstimate, channelQuote.VideoTokenEstimate) {
t.Fatalf("logical and channel quote disagree: logical=%#v channel=%#v", logicalQuote, channelQuote)
}
assertVideoQuoteHasNoFinancialWrites(t, db)
})
}
}
func TestVideoTokenChannelQuoteRejectsUnavailableChannel(t *testing.T) {
for _, test := range []struct {
name string
field string
value any
}{
{name: "disabled channel", field: "enabled", value: false},
{name: "private channel", field: "scope", value: model.ChannelScopeUser},
{name: "disabled model", field: "enabled", value: false},
} {
t.Run(test.name, func(t *testing.T) {
svc, db, channel, channelModel, _ := newVideoTokenQuoteFixture(t)
row, id := any(&model.ModelChannel{}), channel.ID
if test.name == "disabled model" {
row, id = &model.ChannelModel{}, channelModel.ID
}
if err := db.Model(row).Where("id = ?", id).Update(test.field, test.value).Error; err != nil {
t.Fatal(err)
}
quote, err := svc.QuoteChannelModel(ChannelModelQuoteRequest{ChannelID: channel.ID, ModelKey: channelModel.ModelKey, Intent: ModelRequestIntent{Capability: "video"}})
if err == nil || quote != nil {
t.Fatalf("unavailable channel/model must be rejected: quote=%#v error=%v", quote, err)
}
assertVideoQuoteHasNoFinancialWrites(t, db)
})
}
}
func TestVideoTokenQuoteRejectsInvalidReferenceCounts(t *testing.T) {
for _, count := range []int{-1, 129, int(^uint(0) >> 1)} {
for _, kind := range []string{"image", "video", "audio"} {
t.Run(fmt.Sprintf("%s/%d", kind, count), func(t *testing.T) {
intent := ModelRequestIntent{Capability: "video", Inputs: map[string]int{kind: count}}
// A nil repository verifies counts fail before allocation, lookup, or routing.
svc := &Service{}
if quote, err := svc.QuoteChannelModel(ChannelModelQuoteRequest{ChannelID: "system-video", ModelKey: "video", Intent: intent}); err == nil || quote != nil {
t.Fatalf("channel quote must reject invalid count: quote=%#v error=%v", quote, err)
}
if quote, err := svc.QuoteLogicalModel("logical-video", intent); err == nil || quote != nil {
t.Fatalf("logical quote must reject invalid count: quote=%#v error=%v", quote, err)
}
})
}
}
}
func newVideoTokenQuoteFixture(t *testing.T) (*Service, *gorm.DB, model.ModelChannel, model.ChannelModel, *ModelCapabilityConfig) {
t.Helper()
const providerModel = "doubao-seedance-1-5-pro-251215"
profile := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceVolcengineArkVideo), providerModel)
profile.Video.Duration.Default = 5
tiers := []model.ChannelModelPriceTier{
{ID: "video-audio", SelectorKey: `{"videoGenerateAudio":"true"}`, SelectorJSON: `{"videoGenerateAudio":"true"}`, ProviderModelKey: providerModel, BillingMode: "token", OutputTokenPriceMicrocredits: 16_000_000, PriceConfigured: true, Enabled: true},
{ID: "video-silent", SelectorKey: `{"videoGenerateAudio":"false"}`, SelectorJSON: `{"videoGenerateAudio":"false"}`, ProviderModelKey: providerModel, BillingMode: "token", OutputTokenPriceMicrocredits: 8_000_000, PriceConfigured: true, Enabled: true},
}
svc, db, channel, channelModel := createSystemChannelSelectionFixture(t, "video", model.ChannelInterfaceVolcengineArkVideo, profile, tiers)
channelModel.ProviderModelKey = providerModel
if err := db.Model(&model.ChannelModel{}).Where("id = ?", channelModel.ID).Update("provider_model_key", providerModel).Error; err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.SystemSetting{}, &model.CreditAccount{}, &model.CreditLedgerEntry{}, &model.BillingOrder{}); err != nil {
t.Fatal(err)
}
account := model.CreditAccount{UserID: "quote-user", AvailableMicrocredits: 10_000_000, ReservedMicrocredits: 250_000, Version: 7}
if err := db.Create(&account).Error; err != nil {
t.Fatal(err)
}
return svc, db, channel, channelModel, profile
}
func assertVideoTokenQuote(t *testing.T, quote *LogicalModelQuote, amount int64) {
t.Helper()
if quote == nil || quote.BillingMode != "token" || !quote.Estimated || quote.Quantity != 118_800 || quote.AmountMicrocredits != amount {
t.Fatalf("unexpected video quote: %#v, want 118800 tokens and %d microcredits", quote, amount)
}
want := VideoTokenEstimate{
FormulaTokens: 108_000, ReservedTokens: 118_800, OutputWidth: 1280, OutputHeight: 720,
FramesPerSecond: 24, OutputSeconds: 5, ReservationMarginPercent: 10,
}
if quote.VideoTokenEstimate == nil || *quote.VideoTokenEstimate != want {
t.Fatalf("video estimate = %#v, want %#v", quote.VideoTokenEstimate, want)
}
}
func assertVideoQuoteHasNoFinancialWrites(t *testing.T, db *gorm.DB) {
t.Helper()
var account model.CreditAccount
if err := db.First(&account, "user_id = ?", "quote-user").Error; err != nil {
t.Fatal(err)
}
if account.AvailableMicrocredits != 10_000_000 || account.ReservedMicrocredits != 250_000 || account.Version != 7 {
t.Fatalf("read-only quote changed the credit account: %#v", account)
}
for _, row := range []any{&model.BillingOrder{}, &model.CreditLedgerEntry{}, &model.Task{}} {
var count int64
if err := db.Model(row).Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("read-only quote created %d records of %T", count, row)
}
}
}