Files
ttstd-infinite-canvas/backend/internal/app/analytics_finance_test.go
Rou 054dab28cc fix(workbench): 模型目录与助手形象 - 修正 Live2D 首帧闪现和模型默认分组 (#773)
* fix(workbench): 模型目录与助手形象 - 修正 Live2D 首帧闪现和模型默认分组

进入画布时形象配置与 Live2D 首帧未就绪不再显示默认球,改为加载提示,失败才回退。

新导入模型一级目录默认未分类、二级渠道展示名默认跟随上游 ID;历史非空分组(含与模型 ID 同名)保留。

拉取模型弹窗默认只勾选已存在模型并移除逐项已存在字样;一级分组下拉点击即展开全部选项。

* style(web): 画布与模型导入 - 按 Prettier 规范格式化改动文件

修复 PR 中 Web checks 的格式化检查失败:Live2D 启动器 JSX、模型导入选择工具函数与 Live2D 加载测试按项目 Prettier 配置重新排版,不改变行为。

* test(web): Live2D 生命周期 - 修复 bun 1.3.9 下 text 导入与模块导入冲突

agent-live2d-lifecycle 曾用 with { type: text } 导入 live2d-avatar.tsx,bun 1.3.9 会按路径复用该缓存,导致新增的模块导入拿不到 Live2DAvatar 具名导出并报 SyntaxError。改为 readFileSync 读取源码文本,本地以 bun 1.3.9 复现并验证通过。
2026-10-14 09:36:01 +08:00

216 lines
8.5 KiB
Go

package app
import (
"encoding/csv"
"strconv"
"strings"
"testing"
"time"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestAnalyticsFinanceAggregatesKnownAndUnknownCosts(t *testing.T) {
cost := int64(300_000)
zero := int64(0)
for _, test := range []struct {
name string
records []analyticsFinancialRecord
costed int
revenue int64
cost int64
profit *int64
}{
{name: "no orders"},
{name: "explicit zero", records: []analyticsFinancialRecord{{Cost: &zero}}, costed: 1, profit: &zero},
{name: "break even", records: []analyticsFinancialRecord{{Revenue: 300_000, Cost: &cost}}, costed: 1, revenue: 300_000, cost: 300_000, profit: &zero},
{name: "partial cost last", records: []analyticsFinancialRecord{{Revenue: 900_000, Cost: &cost}, {Revenue: 400_000}}, costed: 1, revenue: 1_300_000, cost: 300_000},
{name: "partial cost first", records: []analyticsFinancialRecord{{Revenue: 400_000}, {Revenue: 900_000, Cost: &cost}}, costed: 1, revenue: 1_300_000, cost: 300_000},
} {
t.Run(test.name, func(t *testing.T) {
var finance AnalyticsFinance
for _, record := range test.records {
finance.add(record)
}
if finance.SettledOrders != len(test.records) || finance.CostedOrders != test.costed || finance.RevenueMicrocredits != test.revenue || finance.CostMicrocredits != test.cost {
t.Fatalf("finance = %+v", finance)
}
if test.profit == nil {
if finance.ProfitMicrocredits != nil || finance.ProfitMargin != nil {
t.Fatalf("unknown profit = %+v", finance)
}
return
}
if finance.ProfitMicrocredits == nil || *finance.ProfitMicrocredits != *test.profit {
t.Fatalf("profit = %+v", finance)
}
if test.revenue == 0 {
if finance.ProfitMargin != nil {
t.Fatalf("zero revenue margin = %v", *finance.ProfitMargin)
}
} else if finance.ProfitMargin == nil || *finance.ProfitMargin != float64(*test.profit)*100/float64(test.revenue) {
t.Fatalf("margin = %+v", finance)
}
})
}
}
func TestAdminAnalyticsFinanceUsesSettledSnapshotsAndDeduplicates(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+newID()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.BillingOrder{}, &model.ApiCallLog{}, &model.Task{}, &model.UserDailyActivity{}, &model.User{}); err != nil {
t.Fatal(err)
}
svc := &Service{repo: repository.New(db)}
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
now := time.Now().UTC()
orders := []model.BillingOrder{
{ID: "paid", Status: model.BillingStatusSettled, ActualAmountMicrocredits: 900_000, BillingCostSnapshot: model.BillingCostSnapshot{CostBillingMode: "fixed_request", CostQuantity: 1, CostPricing: model.CreditCostPricing{Configured: true, UnitPriceMicrocredits: 300_000}}},
{ID: "free", Status: model.BillingStatusSettled, BillingCostSnapshot: model.BillingCostSnapshot{CostBillingMode: "fixed_request", CostQuantity: 1, CostPricing: model.CreditCostPricing{Configured: true}}},
{ID: "unknown", Status: model.BillingStatusSettled, ActualAmountMicrocredits: 400_000},
{ID: "reserved", Status: model.BillingStatusReserved, ReservedAmountMicrocredits: 1_000_000},
{ID: "refunded", Status: model.BillingStatusRefunded, ActualAmountMicrocredits: 800_000},
{ID: "loss", Status: model.BillingStatusSettled, ActualAmountMicrocredits: 100_000, BillingCostSnapshot: model.BillingCostSnapshot{CostBillingMode: "per_second", CostQuantity: 3, CostPricing: model.CreditCostPricing{Configured: true, UnitPriceMicrocredits: 100_000}}},
}
logs := make([]model.ApiCallLog, 0)
for index := range orders {
order := &orders[index]
order.UserID, order.ChannelID, order.IdempotencyKey = "user", "channel", order.ID
logs = append(logs, model.ApiCallLog{ID: order.ID, BillingOrderID: order.ID, UserID: "user", ChannelID: "channel", Model: order.ID, Capability: "text", Billable: true, RequestKind: "create", Status: model.ApiCallStatusSucceeded, CreatedAt: now})
}
for _, id := range []string{"retry", "poll", "download", "upload", "workflow-schema", "cancel-query", "cancel", "wrong-user", "old-channel"} {
item := logs[0]
item.ID = id
item.CreatedAt = now.Add(time.Second)
switch id {
case "poll", "download", "upload", "workflow-schema", "cancel-query", "cancel":
item.RequestKind = id
item.Model = "auxiliary"
item.CreatedAt = now.Add(-time.Second)
case "wrong-user":
item.UserID = "other"
case "old-channel":
item.ChannelID = "previous"
}
logs = append(logs, item)
}
if err := db.Create(&orders).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&logs).Error; err != nil {
t.Fatal(err)
}
query := AnalyticsQuery{From: now.Add(-time.Hour).Format(time.RFC3339), To: now.Add(time.Hour).Format(time.RFC3339)}
result, err := svc.AdminAnalytics(admin, query)
if err != nil {
t.Fatal(err)
}
finance := result.KPI.Finance
if finance.SettledOrders != 4 || finance.CostedOrders != 3 || finance.RevenueMicrocredits != 1_400_000 || finance.CostMicrocredits != 600_000 || finance.ProfitMicrocredits != nil || finance.ProfitMargin != nil {
t.Fatalf("partial finance = %+v", finance)
}
for _, row := range result.Models {
f := row.Finance
switch row.Model {
case "paid":
if f.SettledOrders != 1 || f.ProfitMicrocredits == nil || *f.ProfitMicrocredits != 600_000 || f.ProfitMargin == nil || *f.ProfitMargin < 66.6 || *f.ProfitMargin > 66.7 {
t.Fatalf("paid = %+v", f)
}
case "free":
if f.CostedOrders != 1 || f.ProfitMicrocredits == nil || *f.ProfitMicrocredits != 0 || f.ProfitMargin != nil {
t.Fatalf("free = %+v", f)
}
case "loss":
if f.ProfitMicrocredits == nil || *f.ProfitMicrocredits != -200_000 || f.ProfitMargin == nil || *f.ProfitMargin != -200 {
t.Fatalf("loss = %+v", f)
}
}
}
data, err := svc.AdminAnalyticsCSV(admin, query)
if err != nil {
t.Fatal(err)
}
rows, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(string(data), "\xEF\xBB\xBF"))).ReadAll()
if err != nil {
t.Fatal(err)
}
var revenue, cost int64
for _, row := range rows[1:] {
if row[6] == "auxiliary" && (row[15] != "" || row[16] != "" || row[17] != "") {
t.Fatalf("auxiliary request must not carry order finances: %v", row)
}
for index, target := range map[int]*int64{15: &revenue, 16: &cost} {
if row[index] != "" {
value, err := strconv.ParseInt(row[index], 10, 64)
if err != nil {
t.Fatal(err)
}
*target += value
}
}
}
if revenue != finance.RevenueMicrocredits || cost != finance.CostMicrocredits {
t.Fatalf("CSV mismatch: revenue=%d cost=%d", revenue, cost)
}
query.Model = "paid"
filtered, err := svc.AdminAnalytics(admin, query)
if err != nil || filtered.KPI.Finance.SettledOrders != 1 || filtered.KPI.Finance.ProfitMicrocredits == nil || *filtered.KPI.Finance.ProfitMicrocredits != 600_000 {
t.Fatalf("model filter = %+v, %v", filtered, err)
}
query.ChannelID = "previous"
filtered, err = svc.AdminAnalytics(admin, query)
if err != nil || filtered.KPI.Finance.SettledOrders != 0 {
t.Fatalf("previous channel finance = %+v, %v", filtered, err)
}
query.Model, query.ChannelID = "auxiliary", ""
filtered, err = svc.AdminAnalytics(admin, query)
if err != nil || filtered.KPI.Finance.SettledOrders != 0 {
t.Fatalf("auxiliary request finance = %+v, %v", filtered, err)
}
if _, err := svc.AdminAnalytics(&model.User{Role: model.UserRoleUser}, query); err == nil {
t.Fatal("non-admin must not read costs")
}
}
func TestAdminReferencesIncludeExistingDisplayNameGroups(t *testing.T) {
svc, db := newChannelModelTestService(t)
if err := db.AutoMigrate(&model.User{}); err != nil {
t.Fatal(err)
}
channels := []model.ModelChannel{
{ID: "one", Scope: model.ChannelScopeSystem, Enabled: true},
{ID: "two", Scope: model.ChannelScopeSystem, Enabled: false},
}
if err := db.Create(&channels).Error; err != nil {
t.Fatal(err)
}
items := []model.ChannelModel{
{ID: "a", ChannelID: "one", ModelKey: "a", DisplayName: "Shared", Enabled: true},
{ID: "b", ChannelID: "one", ModelKey: "b", DisplayName: "Shared", Enabled: false},
{ID: "c", ChannelID: "two", ModelKey: "c", DisplayName: "Other", Enabled: false},
{ID: "d", ChannelID: "two", ModelKey: "d"},
}
if err := db.Create(&items).Error; err != nil {
t.Fatal(err)
}
result, err := svc.AdminReferences(&model.User{Role: model.UserRoleAdmin})
if err != nil {
t.Fatal(err)
}
groups := map[string]int{}
for _, channel := range result.Channels {
for _, name := range channel.ModelDisplayNames {
groups[name]++
}
}
if len(groups) != 2 || groups["Shared"] != 1 || groups["Other"] != 1 {
t.Fatalf("groups = %#v", groups)
}
}