Files
ttstd-infinite-canvas/backend/internal/app/admin_channel_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

596 lines
24 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 (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"yingce/backend/internal/model"
"yingce/backend/internal/outbound/outboundtest"
"yingce/backend/internal/repository"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestChannelFromRequestStoresConnectionWithoutDefaultProtocol(t *testing.T) {
channel, err := channelFromRequest(ChannelRequest{
Name: "混合模型渠道",
BaseURL: "https://8.8.8.8/v1",
APIKey: "access-key",
SecretKey: "secret-key",
ConcurrencyLimit: intPtr(6),
Models: []string{"seedance-2.0"},
}, model.ModelChannel{})
if err != nil {
t.Fatalf("channelFromRequest() error = %v", err)
}
if channel.APIFormat != "openai" {
t.Fatalf("APIFormat = %q, want openai", channel.APIFormat)
}
if channel.ConcurrencyLimit != 6 {
t.Fatalf("ConcurrencyLimit = %d, want 6", channel.ConcurrencyLimit)
}
if channel.APIKey != "access-key" || channel.SecretKey != "secret-key" {
t.Fatal("channel credentials were not stored")
}
}
func TestMergeChannelRequestSupportsEnabledOnlyPatch(t *testing.T) {
enabled := false
req := mergeChannelRequest(ChannelRequest{Enabled: &enabled}, model.ModelChannel{
Name: "Video",
BaseURL: "https://example.com/v1",
APIFormat: "openai",
ModelsJSON: `["custom-video"]`,
HeadersJSON: `[{"name":"User-Agent","value":"Stored Agent"}]`,
})
if req.Name != "Video" || req.BaseURL != "https://example.com/v1" || len(req.Models) != 1 || len(req.Headers) != 1 {
t.Fatalf("mergeChannelRequest() = %#v", req)
}
}
func TestUpdateSystemChannelEnabledOnlySkipsOutboundResolution(t *testing.T) {
svc, db := newChannelModelTestService(t)
svc.dataDir = t.TempDir()
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Dead", BaseURL: "https://dead.invalid/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
disabled := false
updated, err := svc.UpdateSystemChannel(admin, channel.ID, ChannelRequest{Enabled: &disabled})
if err != nil {
t.Fatalf("UpdateSystemChannel() should not resolve the stored URL for an enabled-only patch: %v", err)
}
if updated.Enabled {
t.Fatal("UpdateSystemChannel() did not persist disabled state")
}
var stored model.ModelChannel
if err := db.First(&stored, "id = ?", channel.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Enabled {
t.Fatal("stored channel is still enabled after update")
}
}
func TestDuplicateSystemChannelCopiesSecretsModelsAndPriceTiers(t *testing.T) {
svc, db := newChannelModelTestService(t)
svc.dataDir = t.TempDir()
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
source := model.ModelChannel{
ID: "channel-source",
UserID: admin.ID,
Scope: model.ChannelScopeSystem,
Enabled: false,
Name: "方舟视频",
PublicAlias: "视频主链路",
BaseURL: "https://example.com/v1",
APIKey: "source-key",
SecretKey: "source-secret",
APIFormat: "openai",
ConcurrencyLimit: 6,
ModelsJSON: `["seedance-2"]`,
HeadersJSON: `[{"name":"X-Tenant","value":"tenant-a"}]`,
}
if err := svc.encryptSystemChannelSecrets(&source); err != nil {
t.Fatal(err)
}
if err := db.Create(&source).Error; err != nil {
t.Fatal(err)
}
sourceModel := model.ChannelModel{
ID: "model-source",
ChannelID: source.ID,
ModelKey: "seedance-2",
ProviderModelKey: "seedance-2",
DisplayName: "Seedance 2",
SortOrder: 3,
Capability: "video",
Protocol: model.ChannelInterfaceVolcengineArkVideo,
BillingMode: "fixed_request",
UnitPriceMicrocredits: 120,
PriceConfigured: true,
Enabled: true,
PriceVersion: 4,
}
if err := db.Create(&sourceModel).Error; err != nil {
t.Fatal(err)
}
sourceTier := model.ChannelModelPriceTier{
ID: "tier-source",
ChannelModelID: sourceModel.ID,
SelectorKey: `{}`,
SelectorJSON: `{}`,
Resolution: "720p",
ProviderModelKey: "seedance-2",
BillingMode: "fixed_request",
UnitPriceMicrocredits: 120,
PriceConfigured: true,
Enabled: true,
}
if err := db.Create(&sourceTier).Error; err != nil {
t.Fatal(err)
}
copied, err := svc.DuplicateSystemChannel(admin, source.ID)
if err != nil {
t.Fatalf("DuplicateSystemChannel() error = %v", err)
}
if copied.Name != "方舟视频 - 副本" || copied.Enabled != source.Enabled {
t.Fatalf("copied channel summary = %#v", copied)
}
var stored model.ModelChannel
if err := db.First(&stored, "id = ?", copied.ID).Error; err != nil {
t.Fatal(err)
}
if err := svc.decryptSystemChannelSecrets(&stored); err != nil {
t.Fatal(err)
}
if stored.APIKey != "source-key" || stored.SecretKey != "source-secret" || stored.HeadersJSON != source.HeadersJSON {
t.Fatalf("copied channel connection = %#v", stored)
}
models, err := svc.repo.ChannelModels(copied.ID, true)
if err != nil {
t.Fatal(err)
}
if len(models) != 1 || models[0].ID == sourceModel.ID || models[0].ModelKey != sourceModel.ModelKey || models[0].SortOrder != sourceModel.SortOrder {
t.Fatalf("copied models = %#v", models)
}
if len(models[0].PriceTiers) != 1 || models[0].PriceTiers[0].ID == sourceTier.ID || models[0].PriceTiers[0].UnitPriceMicrocredits != sourceTier.UnitPriceMicrocredits {
t.Fatalf("copied price tiers = %#v", models[0].PriceTiers)
}
}
func TestChannelFromRequestStoresAndClearsHeaders(t *testing.T) {
// URL validation is real, but this non-network test must not depend on DNS.
request := ChannelRequest{Name: "Headers", BaseURL: "https://93.184.216.34/v1", Headers: []OutboundHeader{{Name: "User-Agent", Value: "Custom Agent"}}}
channel, err := channelFromRequest(request, model.ModelChannel{})
if err != nil {
t.Fatal(err)
}
if channel.HeadersJSON != `[{"name":"User-Agent","value":"Custom Agent"}]` {
t.Fatalf("HeadersJSON = %q", channel.HeadersJSON)
}
request.Headers = []OutboundHeader{}
channel, err = channelFromRequest(request, channel)
if err != nil {
t.Fatal(err)
}
if channel.HeadersJSON != `[]` {
t.Fatalf("cleared HeadersJSON = %q", channel.HeadersJSON)
}
}
func TestChannelFromRequestValidatesProxyURL(t *testing.T) {
limit := 4
base := model.ModelChannel{}
channel, err := channelFromRequest(ChannelRequest{Name: "Proxy", BaseURL: "https://93.184.216.34/v1", ProxyURL: "socks5://user:secret@127.0.0.1:1080", ConcurrencyLimit: &limit}, base)
if err != nil {
t.Fatal(err)
}
if channel.ProxyURL != "socks5://user:secret@127.0.0.1:1080" {
t.Fatalf("proxy URL = %q", channel.ProxyURL)
}
for _, proxyURL := range []string{"ftp://127.0.0.1:1080", "socks5://", "socks5://127.0.0.1:1080#fragment", "http://" + strings.Repeat("a", 506)} {
_, err := channelFromRequest(ChannelRequest{Name: "Proxy", BaseURL: "https://93.184.216.34/v1", ProxyURL: proxyURL, ConcurrencyLimit: &limit}, base)
if err == nil {
t.Fatalf("invalid proxy URL %q was accepted", proxyURL)
}
}
}
func TestPublicChannelOnlyReturnsSystemHeadersAndProxyToAdmin(t *testing.T) {
channel := model.ModelChannel{ID: "system-1", Scope: model.ChannelScopeSystem, BaseURL: "https://example.com/v1", HeadersJSON: `[{"name":"X-Gateway-Tenant","value":"tenant-a"}]`, ProxyURL: "socks5://user:secret@127.0.0.1:1080"}
adminView := publicChannel(channel, true, nil)
if len(adminView.Headers) != 1 || adminView.Headers[0].Name != "X-Gateway-Tenant" {
t.Fatalf("admin headers = %#v", adminView.Headers)
}
if adminView.ProxyURL != channel.ProxyURL {
t.Fatalf("admin proxy URL = %q", adminView.ProxyURL)
}
userView := publicChannel(channel, false, nil)
if len(userView.Headers) != 0 {
t.Fatalf("user headers = %#v", userView.Headers)
}
if userView.ProxyURL != "" {
t.Fatalf("user proxy URL = %q", userView.ProxyURL)
}
}
func TestChannelFromRequestRejectsInvalidConcurrencyLimit(t *testing.T) {
for _, limit := range []int{0, 1000} {
_, err := channelFromRequest(ChannelRequest{Name: "Bad", BaseURL: "https://example.com/v1", ConcurrencyLimit: &limit}, model.ModelChannel{})
if err == nil {
t.Fatalf("channelFromRequest() concurrencyLimit = %d, error = nil", limit)
}
}
}
func TestRuntimeConcurrencyUsesEnvironmentFallback(t *testing.T) {
t.Setenv("CANVAS_CHANNEL_CONCURRENCY", "7")
t.Setenv("CANVAS_WORKER_CONCURRENCY", "9")
setting := defaultRuntimePolicy().Task
if setting.ChannelConcurrency != 7 || setting.WorkerConcurrency != 9 {
t.Fatalf("runtimeConcurrencyFromEnvironment() = %#v", setting)
}
useGlobal := true
channel, err := channelFromRequest(ChannelRequest{Name: "Global", BaseURL: "https://93.184.216.34/v1", UseGlobalConcurrency: &useGlobal}, model.ModelChannel{ConcurrencyLimit: 4})
if err != nil || channel.ConcurrencyLimit != 0 {
t.Fatalf("global concurrency channel = %#v, error = %v", channel, err)
}
}
func TestFetchAdminChannelModelsReaddsDeletedModel(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"model-a"}]}`))
}))
defer upstream.Close()
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: upstream.URL + "/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
deleted := model.ChannelModel{ID: "deleted-model", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "model-a", BillingMode: "fixed_request", PriceVersion: 1}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&deleted).Error; err != nil {
t.Fatal(err)
}
if err := db.Delete(&deleted).Error; err != nil {
t.Fatal(err)
}
result, err := svc.FetchAdminChannelModels(context.Background(), admin, channel.ID)
if err != nil {
t.Fatal(err)
}
if result.Added != 1 {
t.Fatalf("Added = %d, want 1", result.Added)
}
var active model.ChannelModel
if err := db.First(&active, "channel_id = ? AND model_key = ?", channel.ID, "model-a").Error; err != nil {
t.Fatal(err)
}
if active.ID == deleted.ID || active.Enabled || active.PriceConfigured {
t.Fatalf("re-added model = %#v", active)
}
if active.DisplayName != "未分类" || active.ProviderModelKey != "model-a" {
t.Fatalf("discovered model defaults = %#v", active)
}
var total int64
if err := db.Unscoped().Model(&model.ChannelModel{}).Where("channel_id = ? AND model_key = ?", channel.ID, "model-a").Count(&total).Error; err != nil {
t.Fatal(err)
}
if total != 2 {
t.Fatalf("model history count = %d, want 2", total)
}
}
func TestImportAdminChannelModelsOnlyImportsSelectedModels(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"model-a"},{"id":"model-b"}]}`))
}))
defer upstream.Close()
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: upstream.URL + "/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
preview, err := svc.PreviewAdminChannelModels(context.Background(), admin, channel.ID)
if err != nil {
t.Fatal(err)
}
if len(preview) != 2 {
t.Fatalf("preview models = %#v, want two models", preview)
}
var before int64
if err := db.Model(&model.ChannelModel{}).Where("channel_id = ?", channel.ID).Count(&before).Error; err != nil {
t.Fatal(err)
}
if before != 0 {
t.Fatalf("preview created %d channel models", before)
}
result, err := svc.ImportAdminChannelModels(context.Background(), admin, channel.ID, []string{"model-b"})
if err != nil {
t.Fatal(err)
}
if result.Added != 1 || len(result.Models) != 1 || result.Models[0] != "model-b" {
t.Fatalf("import result = %#v, want only model-b", result)
}
var imported []model.ChannelModel
if err := db.Where("channel_id = ?", channel.ID).Find(&imported).Error; err != nil {
t.Fatal(err)
}
if len(imported) != 1 || imported[0].ModelKey != "model-b" {
t.Fatalf("imported models = %#v, want only model-b", imported)
}
if imported[0].DisplayName != "未分类" || imported[0].ProviderModelKey != "model-b" {
t.Fatalf("imported defaults = %#v", imported)
}
// 已有分组即使与模型 ID 相同,也不能被重复导入重置。
if err := db.Model(&imported[0]).Updates(map[string]any{"display_name": "model-b", "channel_label": "手动名称"}).Error; err != nil {
t.Fatal(err)
}
result, err = svc.ImportAdminChannelModels(context.Background(), admin, channel.ID, []string{"model-b"})
if err != nil || result.Added != 0 {
t.Fatalf("duplicate import = %#v, %v", result, err)
}
var unchanged model.ChannelModel
if err := db.First(&unchanged, "id = ?", imported[0].ID).Error; err != nil {
t.Fatal(err)
}
if unchanged.DisplayName != "model-b" || unchanged.ChannelLabel != "手动名称" {
t.Fatalf("duplicate import overwrote existing names: %#v", unchanged)
}
}
func TestImportAdminChannelModelsRejectsUnknownSelection(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"model-a"}]}`))
}))
defer upstream.Close()
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: upstream.URL + "/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
_, err := svc.ImportAdminChannelModels(context.Background(), admin, channel.ID, []string{"not-in-catalog"})
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Message != "所选模型不在上游模型目录中:not-in-catalog" {
t.Fatalf("ImportAdminChannelModels() error = %#v", err)
}
var count int64
if err := db.Model(&model.ChannelModel{}).Where("channel_id = ?", channel.ID).Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("unknown selection created %d channel models", count)
}
}
func TestSaveAdminChannelModelRejectsActiveDuplicateKey(t *testing.T) {
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: "https://example.com/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
items := []model.ChannelModel{
{ID: "model-a", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "Model A", Capability: "text", Protocol: model.ChannelInterfaceChatCompletion, BillingMode: "fixed_request", Enabled: true, PriceVersion: 1},
{ID: "model-b", ChannelID: channel.ID, ModelKey: "model-b", DisplayName: "Model B", Capability: "text", Protocol: model.ChannelInterfaceChatCompletion, BillingMode: "fixed_request", Enabled: true, PriceVersion: 1},
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&items).Error; err != nil {
t.Fatal(err)
}
enabled := true
_, err := svc.SaveAdminChannelModel(admin, channel.ID, items[0].ID, ChannelModelRequest{ModelKey: "model-b", DisplayName: "Duplicate", Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion), BillingMode: "fixed_request", Enabled: &enabled})
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Status != http.StatusBadRequest || authErr.Message != "该渠道已存在模型 model-b,请直接编辑已有模型" {
t.Fatalf("SaveAdminChannelModel() error = %#v", err)
}
}
func TestDeleteAdminChannelModelsDeletesSelectionAtomically(t *testing.T) {
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: "https://example.com/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `["model-a","model-b","model-c"]`}
items := []model.ChannelModel{
{ID: "model-a", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "Model A", Enabled: true, PriceVersion: 1},
{ID: "model-b", ChannelID: channel.ID, ModelKey: "model-b", DisplayName: "Model B", Enabled: true, PriceVersion: 1},
{ID: "model-c", ChannelID: channel.ID, ModelKey: "model-c", DisplayName: "Model C", Enabled: true, PriceVersion: 1},
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&items).Error; err != nil {
t.Fatal(err)
}
deleted, err := svc.DeleteAdminChannelModels(admin, channel.ID, []string{" model-a ", "model-b", "model-a"})
if err != nil {
t.Fatal(err)
}
if deleted != 2 {
t.Fatalf("deleted = %d, want 2", deleted)
}
var storedChannel model.ModelChannel
if err := db.First(&storedChannel, "id = ?", channel.ID).Error; err != nil {
t.Fatal(err)
}
if storedChannel.ModelsJSON != `["model-c"]` {
t.Fatalf("ModelsJSON = %s, want model-c only", storedChannel.ModelsJSON)
}
var active []model.ChannelModel
if err := db.Where("channel_id = ?", channel.ID).Find(&active).Error; err != nil {
t.Fatal(err)
}
if len(active) != 1 || active[0].ID != "model-c" {
t.Fatalf("active models = %#v, want model-c", active)
}
var removed []model.ChannelModel
if err := db.Unscoped().Where("channel_id = ? AND id IN ?", channel.ID, []string{"model-a", "model-b"}).Find(&removed).Error; err != nil {
t.Fatal(err)
}
if len(removed) != 2 {
t.Fatalf("removed models = %#v, want two", removed)
}
for _, item := range removed {
if item.Enabled || item.PriceVersion != 2 || !item.DeletedAt.Valid {
t.Fatalf("removed model state = %#v", item)
}
}
}
func TestDeleteAdminChannelModelsRejectsWholeSelectionWhenOneModelIsInUse(t *testing.T) {
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: "https://example.com/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `["model-a","model-b"]`}
items := []model.ChannelModel{
{ID: "model-a", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "Model A", Enabled: true, PriceVersion: 1},
{ID: "model-b", ChannelID: channel.ID, ModelKey: "model-b", DisplayName: "Model B", Enabled: true, PriceVersion: 1},
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&items).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&model.Task{ID: "task-running", ChannelModelID: "model-b", Status: model.TaskStatusRunning}).Error; err != nil {
t.Fatal(err)
}
deleted, err := svc.DeleteAdminChannelModels(admin, channel.ID, []string{"model-a", "model-b"})
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Message != "所选渠道模型中有模型仍被前台模型供应线路或进行中任务使用,本次未删除任何模型" {
t.Fatalf("DeleteAdminChannelModels() deleted = %d, error = %#v", deleted, err)
}
var active int64
if err := db.Model(&model.ChannelModel{}).Where("channel_id = ?", channel.ID).Count(&active).Error; err != nil {
t.Fatal(err)
}
if active != 2 {
t.Fatalf("active models = %d, want 2 after atomic rejection", active)
}
var storedChannel model.ModelChannel
if err := db.First(&storedChannel, "id = ?", channel.ID).Error; err != nil {
t.Fatal(err)
}
if storedChannel.ModelsJSON != channel.ModelsJSON {
t.Fatalf("ModelsJSON changed after rejected batch: %s", storedChannel.ModelsJSON)
}
}
func TestNormalizeAdminChannelModelDeleteIDsRequiresBoundedSelection(t *testing.T) {
if _, err := normalizeAdminChannelModelDeleteIDs([]string{"", " "}); err == nil {
t.Fatal("empty selection should be rejected")
}
values := make([]string, 101)
for index := range values {
values[index] = "model-" + strconv.Itoa(index)
}
if _, err := normalizeAdminChannelModelDeleteIDs(values); err == nil {
t.Fatal("selection above 100 models should be rejected")
}
}
func TestResolveProviderConfigMapsSKUToProviderModel(t *testing.T) {
outboundtest.PublicDNS(t, "ark.cn-beijing.volces.com")
svc, db := newChannelModelTestService(t)
svc.dataDir = t.TempDir()
channel := model.ModelChannel{
ID: "channel-1", Scope: model.ChannelScopeSystem, Enabled: true, Name: "Seedance",
BaseURL: "https://ark.cn-beijing.volces.com/api/v3", APIKey: "test-key", APIFormat: "openai", ModelsJSON: `["seedance-2-5-480p"]`,
}
if err := svc.encryptSystemChannelSecrets(&channel); err != nil {
t.Fatal(err)
}
item := model.ChannelModel{
ID: "model-1", ChannelID: channel.ID, ModelKey: "seedance-2-5-480p", ProviderModelKey: "doubao-seedance-2-5",
Capability: "video", Protocol: model.ChannelInterfaceVolcengineArkVideo, Enabled: true,
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&item).Error; err != nil {
t.Fatal(err)
}
config, err := svc.resolveProviderConfig(providerConfig{ChannelID: channel.ID, Model: item.ModelKey})
if err != nil {
t.Fatal(err)
}
if config.ChannelModelKey != item.ModelKey || config.Model != item.ProviderModelKey {
t.Fatalf("resolved config = %#v", config)
}
}
func TestValidateTaskCapabilityFixesSingleResolutionSKU(t *testing.T) {
svc, db := newChannelModelTestService(t)
video := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceVolcengineArkVideo), "doubao-seedance-2-5").Video
video.References.MaxVideos = 0
video.Resolutions = []string{"480p"}
video.DefaultResolution = "480p"
encoded, err := json.Marshal(&ModelCapabilityConfig{Version: 1, Video: video})
if err != nil {
t.Fatal(err)
}
channelModel := model.ChannelModel{
ID: "model-480p", ChannelID: "channel-1", ModelKey: "doubao-seedance-2-5-480p", ProviderModelKey: "doubao-seedance-2-5",
Capability: "video", Protocol: model.ChannelInterfaceVolcengineArkVideo, Enabled: true, CapabilityConfigJSON: string(encoded),
}
if err := db.Create(&channelModel).Error; err != nil {
t.Fatal(err)
}
input := map[string]any{
"mode": "video",
"config": map[string]any{
"channelId": "channel-1", "model": channelModel.ModelKey, "vquality": "auto", "videoSeconds": "6", "size": "16:9",
"videoGenerateAudio": "true", "videoWatermark": "false",
},
}
if err := svc.ValidateTaskCapability(input); err != nil {
t.Fatal(err)
}
config := input["config"].(map[string]any)
if got := config["vquality"]; got != "480p" {
t.Fatalf("vquality = %#v, want 480p", got)
}
}
func newChannelModelTestService(t *testing.T) (*Service, *gorm.DB) {
t.Helper()
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.ModelChannel{}, &model.ChannelModel{}, &model.ChannelModelPriceTier{}, &model.LogicalModel{}, &model.LogicalModelRevision{}, &model.LogicalModelRoute{}, &model.Task{}, &model.IDSequence{}); err != nil {
t.Fatal(err)
}
return &Service{repo: repository.New(db)}, db
}
func intPtr(value int) *int { return &value }