547 lines
21 KiB
Go
547 lines
21 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strconv"
|
||
"testing"
|
||
|
||
"infinite-canvas/backend/internal/model"
|
||
"infinite-canvas/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) {
|
||
request := ChannelRequest{Name: "Headers", BaseURL: "https://example.com/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 TestPublicChannelOnlyReturnsSystemHeadersToAdmin(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"}]`}
|
||
adminView := publicChannel(channel, true, nil)
|
||
if len(adminView.Headers) != 1 || adminView.Headers[0].Name != "X-Gateway-Tenant" {
|
||
t.Fatalf("admin headers = %#v", adminView.Headers)
|
||
}
|
||
userView := publicChannel(channel, false, nil)
|
||
if len(userView.Headers) != 0 {
|
||
t.Fatalf("user headers = %#v", userView.Headers)
|
||
}
|
||
}
|
||
|
||
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://example.com/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)
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
|
||
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) {
|
||
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 }
|