ccpoad / internal /storage /sql /config_test.go
anyalerob's picture
Upload folder using huggingface_hub
2986042 verified
Raw
History Blame Contribute Delete
27 kB
package sql_test
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"testing"
"time"
"ccLoad/internal/model"
"ccLoad/internal/storage"
_ "modernc.org/sqlite"
)
func TestConfig_CreateAndGet(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
store, err := storage.CreateSQLiteStore(filepath.Join(tmp, "config.db"))
if err != nil {
t.Fatalf("create sqlite store: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
ctx := context.Background()
// 创建渠道
cfg := &model.Config{
Name: "test-channel",
URL: "https://api.openai.com",
Priority: 10,
Enabled: true,
ChannelType: "openai",
RPMLimit: 60,
ModelEntries: []model.ModelEntry{
{Model: "gpt-4"},
{Model: "gpt-3.5-turbo"},
},
}
created, err := store.CreateConfig(ctx, cfg)
if err != nil {
t.Fatalf("create config: %v", err)
}
if created.ID == 0 {
t.Error("expected non-zero ID")
}
// 获取渠道
got, err := store.GetConfig(ctx, created.ID)
if err != nil {
t.Fatalf("get config: %v", err)
}
if got.Name != "test-channel" {
t.Errorf("name: got %q, want %q", got.Name, "test-channel")
}
if got.URL != "https://api.openai.com" {
t.Errorf("url: got %q, want %q", got.URL, "https://api.openai.com")
}
if got.Priority != 10 {
t.Errorf("priority: got %d, want %d", got.Priority, 10)
}
if !got.Enabled {
t.Error("expected enabled=true")
}
if got.ChannelType != "openai" {
t.Errorf("channel_type: got %q, want %q", got.ChannelType, "openai")
}
if got.RPMLimit != 60 {
t.Errorf("rpm_limit: got %d, want 60", got.RPMLimit)
}
if len(got.ModelEntries) != 2 {
t.Errorf("model entries count: got %d, want 2", len(got.ModelEntries))
}
// 获取不存在的渠道
_, err = store.GetConfig(ctx, 99999)
if err == nil {
t.Error("expected error for non-existent config")
}
}
func TestConfig_ListConfigs(t *testing.T) {
t.Parallel()
store := newTestStore(t, "list.db")
ctx := context.Background()
// 创建多个渠道
for i := 1; i <= 3; i++ {
cfg := &model.Config{
Name: fmt.Sprintf("channel-%c", rune('A'+i-1)),
URL: "https://api.example.com",
Priority: i * 10,
Enabled: true,
ModelEntries: []model.ModelEntry{
{Model: fmt.Sprintf("model-%c", rune('a'+i-1))},
},
}
if _, err := store.CreateConfig(ctx, cfg); err != nil {
t.Fatalf("create config %d: %v", i, err)
}
}
// 列出所有渠道
configs, err := store.ListConfigs(ctx)
if err != nil {
t.Fatalf("list configs: %v", err)
}
if len(configs) != 3 {
t.Errorf("expected 3 configs, got %d", len(configs))
}
// 验证按优先级降序排列
for i := 1; i < len(configs); i++ {
if configs[i-1].Priority < configs[i].Priority {
t.Errorf("configs not sorted by priority DESC: %d < %d",
configs[i-1].Priority, configs[i].Priority)
}
}
}
func TestConfig_UpdateChannelEnabledOnlyTouchesEnabled(t *testing.T) {
t.Parallel()
store := newTestStore(t, "update-enabled.db")
ctx := context.Background()
created, err := store.CreateConfig(ctx, &model.Config{
Name: "toggle-only",
URL: "https://api.example.com",
Priority: 42,
Enabled: true,
ChannelType: "gemini",
ProtocolTransformMode: model.ProtocolTransformModeUpstream,
ProtocolTransforms: []string{"openai", "anthropic"},
ModelEntries: []model.ModelEntry{
{Model: "gemini-2.5-pro", RedirectModel: "gemini-2.5-pro"},
{Model: "gemini-2.5-flash", RedirectModel: "flash-upstream"},
},
})
if err != nil {
t.Fatalf("create config: %v", err)
}
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{{
ChannelID: created.ID,
KeyIndex: 0,
APIKey: "sk-toggle",
KeyStrategy: model.KeyStrategyRoundRobin,
}}); err != nil {
t.Fatalf("create api key: %v", err)
}
updated, err := store.UpdateChannelEnabled(ctx, created.ID, false)
if err != nil {
t.Fatalf("update enabled: %v", err)
}
if updated.Enabled {
t.Fatalf("updated response should be disabled")
}
got, err := store.GetConfig(ctx, created.ID)
if err != nil {
t.Fatalf("get config: %v", err)
}
if got.Enabled {
t.Fatalf("stored channel should be disabled")
}
if got.Name != created.Name || got.URL != created.URL || got.Priority != created.Priority || got.ChannelType != created.ChannelType {
t.Fatalf("non-enabled fields changed: got=%+v want=%+v", got, created)
}
if got.GetProtocolTransformMode() != model.ProtocolTransformModeUpstream {
t.Fatalf("protocol transform mode changed: %q", got.GetProtocolTransformMode())
}
if strings.Join(got.ProtocolTransforms, ",") != "anthropic,openai" {
t.Fatalf("protocol transforms changed: %#v", got.ProtocolTransforms)
}
if len(got.ModelEntries) != 2 || got.ModelEntries[1].RedirectModel != "flash-upstream" {
t.Fatalf("model entries changed: %#v", got.ModelEntries)
}
keys, err := store.GetAPIKeys(ctx, created.ID)
if err != nil {
t.Fatalf("get api keys: %v", err)
}
if len(keys) != 1 || keys[0].APIKey != "sk-toggle" || keys[0].KeyStrategy != model.KeyStrategyRoundRobin {
t.Fatalf("api keys changed: %#v", keys)
}
}
func TestConfig_UpdateConfig(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
store, err := storage.CreateSQLiteStore(filepath.Join(tmp, "update.db"))
if err != nil {
t.Fatalf("create sqlite store: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
ctx := context.Background()
// 创建渠道
cfg := &model.Config{
Name: "original-name",
URL: "https://old.api.com",
Priority: 1,
Enabled: true,
ModelEntries: []model.ModelEntry{
{Model: "old-model"},
},
}
created, err := store.CreateConfig(ctx, cfg)
if err != nil {
t.Fatalf("create config: %v", err)
}
// 更新渠道
created.Name = "updated-name"
created.URL = "https://new.api.com"
created.Priority = 100
created.Enabled = false
created.ModelEntries = []model.ModelEntry{
{Model: "new-model-1"},
{Model: "new-model-2"},
}
if _, err := store.UpdateConfig(ctx, created.ID, created); err != nil {
t.Fatalf("update config: %v", err)
}
// 验证更新
got, err := store.GetConfig(ctx, created.ID)
if err != nil {
t.Fatalf("get config after update: %v", err)
}
if got.Name != "updated-name" {
t.Errorf("name: got %q, want %q", got.Name, "updated-name")
}
if got.URL != "https://new.api.com" {
t.Errorf("url: got %q, want %q", got.URL, "https://new.api.com")
}
if got.Priority != 100 {
t.Errorf("priority: got %d, want %d", got.Priority, 100)
}
if got.Enabled {
t.Error("expected enabled=false")
}
if len(got.ModelEntries) != 2 {
t.Errorf("model entries count: got %d, want 2", len(got.ModelEntries))
}
}
func TestConfig_DeleteConfig(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
store, err := storage.CreateSQLiteStore(filepath.Join(tmp, "delete.db"))
if err != nil {
t.Fatalf("create sqlite store: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
ctx := context.Background()
// 创建渠道
cfg := &model.Config{
Name: "to-delete",
URL: "https://api.example.com",
Priority: 1,
Enabled: true,
}
created, err := store.CreateConfig(ctx, cfg)
if err != nil {
t.Fatalf("create config: %v", err)
}
// 删除渠道
if err := store.DeleteConfig(ctx, created.ID); err != nil {
t.Fatalf("delete config: %v", err)
}
// 验证已删除
_, err = store.GetConfig(ctx, created.ID)
if err == nil {
t.Error("expected error after delete")
}
}
func TestConfig_DeleteConfig_AllowsRecreateWithSameIDAndKeyIndicesInMemoryStore(t *testing.T) {
t.Parallel()
store, err := storage.CreateSQLiteStore(":memory:")
if err != nil {
t.Fatalf("create sqlite store: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
ctx := context.Background()
created, err := store.CreateConfig(ctx, &model.Config{
Name: "to-delete-memory",
URL: "https://api.example.com",
Enabled: true,
})
if err != nil {
t.Fatalf("create config: %v", err)
}
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: created.ID, KeyIndex: 0, APIKey: "sk-old-0", KeyStrategy: model.KeyStrategySequential},
{ChannelID: created.ID, KeyIndex: 1, APIKey: "sk-old-1", KeyStrategy: model.KeyStrategySequential},
}); err != nil {
t.Fatalf("create api keys batch: %v", err)
}
if err := store.DeleteConfig(ctx, created.ID); err != nil {
t.Fatalf("delete config: %v", err)
}
recreated, err := store.CreateConfig(ctx, &model.Config{
ID: created.ID,
Name: "recreated-memory",
URL: "https://api-recreated.example.com",
Enabled: true,
})
if err != nil {
t.Fatalf("recreate config with explicit id: %v", err)
}
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: recreated.ID, KeyIndex: 0, APIKey: "sk-new-0", KeyStrategy: model.KeyStrategySequential},
}); err != nil {
t.Fatalf("create api key after recreate: %v", err)
}
keys, err := store.GetAPIKeys(ctx, recreated.ID)
if err != nil {
t.Fatalf("get recreated api keys: %v", err)
}
if len(keys) != 1 {
t.Fatalf("expected 1 recreated api key, got %d", len(keys))
}
}
func TestConfig_GetEnabledChannelsByModel(t *testing.T) {
t.Parallel()
store := newTestStore(t, "model_query.db")
ctx := context.Background()
// 创建启用的渠道支持 gpt-4
cfg1 := &model.Config{
Name: "gpt4-channel",
URL: "https://api.openai.com",
Priority: 10,
Enabled: true,
ModelEntries: []model.ModelEntry{
{Model: "gpt-4"},
{Model: "gpt-4-turbo"},
},
}
created1, err := store.CreateConfig(ctx, cfg1)
if err != nil {
t.Fatalf("create config 1: %v", err)
}
// 创建启用的渠道支持 claude
cfg2 := &model.Config{
Name: "claude-channel",
URL: "https://api.anthropic.com",
Priority: 20,
Enabled: true,
ModelEntries: []model.ModelEntry{
{Model: "claude-3-opus"},
},
}
created2, err := store.CreateConfig(ctx, cfg2)
if err != nil {
t.Fatalf("create config 2: %v", err)
}
// 创建禁用的渠道支持 gpt-4
cfg3 := &model.Config{
Name: "disabled-channel",
URL: "https://api.disabled.com",
Priority: 30,
Enabled: false,
ModelEntries: []model.ModelEntry{
{Model: "gpt-4"},
},
}
if _, err := store.CreateConfig(ctx, cfg3); err != nil {
t.Fatalf("create config 3: %v", err)
}
// 为渠道添加 API Key(需要至少有一个 key 才能被选中)
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: created1.ID, KeyIndex: 0, APIKey: "sk-key1"},
}); err != nil {
t.Fatalf("create api keys batch for channel 1: %v", err)
}
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: created2.ID, KeyIndex: 0, APIKey: "sk-key2"},
}); err != nil {
t.Fatalf("create api keys batch for channel 2: %v", err)
}
// 查询支持 gpt-4 的启用渠道
configs, err := store.GetEnabledChannelsByModel(ctx, "gpt-4")
if err != nil {
t.Fatalf("get enabled channels by model: %v", err)
}
if len(configs) != 1 {
t.Errorf("expected 1 enabled channel with gpt-4, got %d", len(configs))
}
if len(configs) > 0 && configs[0].Name != "gpt4-channel" {
t.Errorf("expected gpt4-channel, got %s", configs[0].Name)
}
// 通配符查询所有启用渠道
allConfigs, err := store.GetEnabledChannelsByModel(ctx, "*")
if err != nil {
t.Fatalf("get all enabled channels: %v", err)
}
if len(allConfigs) != 2 {
t.Errorf("expected 2 enabled channels, got %d", len(allConfigs))
}
}
func TestConfig_GetEnabledChannelsIncludesCooledEnabledChannels(t *testing.T) {
t.Parallel()
store := newTestStore(t, "enabled_includes_cooled.db")
ctx := context.Background()
cooled, err := store.CreateConfig(ctx, &model.Config{
Name: "cooled-enabled",
URL: "https://api.example.com",
Priority: 100,
Enabled: true,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4o"},
},
})
if err != nil {
t.Fatalf("create cooled config: %v", err)
}
if err := store.SetChannelCooldown(ctx, cooled.ID, time.Now().Add(2*time.Minute)); err != nil {
t.Fatalf("set channel cooldown: %v", err)
}
disabled, err := store.CreateConfig(ctx, &model.Config{
Name: "disabled",
URL: "https://disabled.example.com",
Priority: 90,
Enabled: false,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4o"},
},
})
if err != nil {
t.Fatalf("create disabled config: %v", err)
}
assertHasOnlyCooled := func(name string, configs []*model.Config) {
t.Helper()
foundCooled := false
for _, cfg := range configs {
switch cfg.ID {
case cooled.ID:
foundCooled = true
case disabled.ID:
t.Fatalf("%s returned disabled channel: %+v", name, cfg)
}
}
if !foundCooled {
t.Fatalf("%s did not return cooled enabled channel; got %+v", name, configs)
}
}
byModel, err := store.GetEnabledChannelsByModel(ctx, "gpt-4o")
if err != nil {
t.Fatalf("GetEnabledChannelsByModel: %v", err)
}
assertHasOnlyCooled("GetEnabledChannelsByModel", byModel)
allByModel, err := store.GetEnabledChannelsByModel(ctx, "*")
if err != nil {
t.Fatalf("GetEnabledChannelsByModel(*): %v", err)
}
assertHasOnlyCooled("GetEnabledChannelsByModel(*)", allByModel)
byType, err := store.GetEnabledChannelsByType(ctx, "openai")
if err != nil {
t.Fatalf("GetEnabledChannelsByType: %v", err)
}
assertHasOnlyCooled("GetEnabledChannelsByType", byType)
byProtocol, err := store.GetEnabledChannelsByExposedProtocol(ctx, "openai")
if err != nil {
t.Fatalf("GetEnabledChannelsByExposedProtocol: %v", err)
}
assertHasOnlyCooled("GetEnabledChannelsByExposedProtocol", byProtocol)
byModelAndProtocol, err := store.GetEnabledChannelsByModelAndProtocol(ctx, "gpt-4o", "openai")
if err != nil {
t.Fatalf("GetEnabledChannelsByModelAndProtocol: %v", err)
}
assertHasOnlyCooled("GetEnabledChannelsByModelAndProtocol", byModelAndProtocol)
}
func TestConfig_GetEnabledChannelsByType(t *testing.T) {
t.Parallel()
store := newTestStore(t, "type_query.db")
ctx := context.Background()
// 创建 openai 类型渠道
cfg1 := &model.Config{
Name: "openai-channel",
URL: "https://api.openai.com",
Priority: 10,
Enabled: true,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4"},
},
}
created1, err := store.CreateConfig(ctx, cfg1)
if err != nil {
t.Fatalf("create openai config: %v", err)
}
// 创建 anthropic 类型渠道
cfg2 := &model.Config{
Name: "anthropic-channel",
URL: "https://api.anthropic.com",
Priority: 20,
Enabled: true,
ChannelType: "anthropic",
ModelEntries: []model.ModelEntry{
{Model: "claude-3"},
},
}
created2, err := store.CreateConfig(ctx, cfg2)
if err != nil {
t.Fatalf("create anthropic config: %v", err)
}
// 添加 API Key
_ = store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: created1.ID, KeyIndex: 0, APIKey: "sk-openai"},
})
_ = store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: created2.ID, KeyIndex: 0, APIKey: "sk-anthropic"},
})
// 按类型查询
openaiChannels, err := store.GetEnabledChannelsByType(ctx, "openai")
if err != nil {
t.Fatalf("get openai channels: %v", err)
}
if len(openaiChannels) != 1 {
t.Errorf("expected 1 openai channel, got %d", len(openaiChannels))
}
anthropicChannels, err := store.GetEnabledChannelsByType(ctx, "anthropic")
if err != nil {
t.Fatalf("get anthropic channels: %v", err)
}
if len(anthropicChannels) != 1 {
t.Errorf("expected 1 anthropic channel, got %d", len(anthropicChannels))
}
}
func TestConfig_GetEnabledChannelsByExposedProtocol(t *testing.T) {
t.Parallel()
store := newTestStore(t, "protocol_query.db")
ctx := context.Background()
cfg := &model.Config{
Name: "gemini-openai-channel",
URL: "https://generativelanguage.googleapis.com",
Priority: 10,
Enabled: true,
ChannelType: "gemini",
ProtocolTransforms: []string{"openai"},
ModelEntries: []model.ModelEntry{
{Model: "gemini-2.5-pro"},
},
}
created, err := store.CreateConfig(ctx, cfg)
if err != nil {
t.Fatalf("create gemini config: %v", err)
}
nativeOpenAI, err := store.CreateConfig(ctx, &model.Config{
Name: "native-openai-channel",
URL: "https://api.openai.com",
Priority: 20,
Enabled: true,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4.1"},
},
})
if err != nil {
t.Fatalf("create native openai config: %v", err)
}
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: created.ID, KeyIndex: 0, APIKey: "sk-gemini"},
{ChannelID: nativeOpenAI.ID, KeyIndex: 0, APIKey: "sk-openai"},
}); err != nil {
t.Fatalf("create api keys batch: %v", err)
}
openaiChannels, err := store.GetEnabledChannelsByExposedProtocol(ctx, "openai")
if err != nil {
t.Fatalf("get openai exposed channels: %v", err)
}
if len(openaiChannels) != 2 {
t.Fatalf("expected 2 openai-exposed channels, got %d", len(openaiChannels))
}
if openaiChannels[0].Name != "native-openai-channel" {
t.Fatalf("expected native channel first by priority, got %s", openaiChannels[0].Name)
}
if openaiChannels[1].Name != "gemini-openai-channel" {
t.Fatalf("expected transformed channel second, got %s", openaiChannels[1].Name)
}
if len(openaiChannels[1].ProtocolTransforms) != 1 || openaiChannels[1].ProtocolTransforms[0] != "openai" {
t.Fatalf("unexpected protocol transforms: %#v", openaiChannels[1].ProtocolTransforms)
}
geminiChannels, err := store.GetEnabledChannelsByExposedProtocol(ctx, "gemini")
if err != nil {
t.Fatalf("get gemini exposed channels: %v", err)
}
if len(geminiChannels) != 1 {
t.Fatalf("expected 1 gemini-exposed channel, got %d", len(geminiChannels))
}
if geminiChannels[0].Name != "gemini-openai-channel" {
t.Fatalf("unexpected gemini channel name: %s", geminiChannels[0].Name)
}
}
func TestConfig_GetConfig_EmitsDefaultProtocolTransformMode(t *testing.T) {
t.Parallel()
store := newTestStore(t, "protocol_transform_mode_default.db")
ctx := context.Background()
created, err := store.CreateConfig(ctx, &model.Config{
Name: "default-transform-mode",
URL: "https://api.example.com",
Priority: 10,
Enabled: true,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4.1"},
},
})
if err != nil {
t.Fatalf("create config: %v", err)
}
got, err := store.GetConfig(ctx, created.ID)
if err != nil {
t.Fatalf("get config: %v", err)
}
body, err := json.Marshal(got)
if err != nil {
t.Fatalf("marshal config: %v", err)
}
if !strings.Contains(string(body), `"protocol_transform_mode":"upstream"`) {
t.Fatalf("期望默认输出 protocol_transform_mode=upstream,实际 JSON: %s", body)
}
}
func TestConfig_GetEnabledChannelsByModelAndProtocol(t *testing.T) {
t.Parallel()
store := newTestStore(t, "model_protocol_query.db")
ctx := context.Background()
openAIChannel, err := store.CreateConfig(ctx, &model.Config{
Name: "openai-native",
URL: "https://api.openai.com",
Priority: 30,
Enabled: true,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4o"},
},
})
if err != nil {
t.Fatalf("create openai config: %v", err)
}
geminiTransform, err := store.CreateConfig(ctx, &model.Config{
Name: "gemini-openai-transform",
URL: "https://generativelanguage.googleapis.com",
Priority: 20,
Enabled: true,
ChannelType: "gemini",
ProtocolTransforms: []string{"openai"},
ModelEntries: []model.ModelEntry{
{Model: "gemini-2.5-pro"},
},
})
if err != nil {
t.Fatalf("create gemini transform config: %v", err)
}
anthropicTransform, err := store.CreateConfig(ctx, &model.Config{
Name: "gemini-anthropic-transform",
URL: "https://generativelanguage.googleapis.com",
Priority: 10,
Enabled: true,
ChannelType: "gemini",
ProtocolTransforms: []string{"anthropic"},
ModelEntries: []model.ModelEntry{
{Model: "claude-3-5-sonnet"},
},
})
if err != nil {
t.Fatalf("create anthropic transform config: %v", err)
}
codexTransform, err := store.CreateConfig(ctx, &model.Config{
Name: "openai-codex-transform",
URL: "https://api.openai.com",
Priority: 15,
Enabled: true,
ChannelType: "openai",
ProtocolTransforms: []string{"codex"},
ModelEntries: []model.ModelEntry{
{Model: "gpt-5-codex"},
},
})
if err != nil {
t.Fatalf("create codex transform config: %v", err)
}
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: openAIChannel.ID, KeyIndex: 0, APIKey: "sk-openai"},
{ChannelID: geminiTransform.ID, KeyIndex: 0, APIKey: "sk-gemini"},
{ChannelID: anthropicTransform.ID, KeyIndex: 0, APIKey: "sk-anthropic"},
{ChannelID: codexTransform.ID, KeyIndex: 0, APIKey: "sk-codex"},
}); err != nil {
t.Fatalf("create api keys batch: %v", err)
}
exact, err := store.GetEnabledChannelsByModelAndProtocol(ctx, "gemini-2.5-pro", "openai")
if err != nil {
t.Fatalf("query exact model+protocol: %v", err)
}
if len(exact) != 1 || exact[0].Name != "gemini-openai-transform" {
t.Fatalf("unexpected exact query result: %+v", exact)
}
wildcard, err := store.GetEnabledChannelsByModelAndProtocol(ctx, "*", "openai")
if err != nil {
t.Fatalf("query wildcard model+protocol: %v", err)
}
if len(wildcard) != 3 {
t.Fatalf("expected 3 openai-exposed channels, got %d", len(wildcard))
}
if wildcard[0].Name != "openai-native" || wildcard[1].Name != "gemini-openai-transform" || wildcard[2].Name != "openai-codex-transform" {
t.Fatalf("unexpected wildcard ordering/result: %+v", wildcard)
}
anthropicExact, err := store.GetEnabledChannelsByModelAndProtocol(ctx, "claude-3-5-sonnet", "anthropic")
if err != nil {
t.Fatalf("query anthropic transform: %v", err)
}
if len(anthropicExact) != 1 || anthropicExact[0].Name != "gemini-anthropic-transform" {
t.Fatalf("unexpected anthropic exact result: %+v", anthropicExact)
}
codexExact, err := store.GetEnabledChannelsByModelAndProtocol(ctx, "gpt-5-codex", "codex")
if err != nil {
t.Fatalf("query codex transform: %v", err)
}
if len(codexExact) != 1 || codexExact[0].Name != "openai-codex-transform" {
t.Fatalf("unexpected codex exact result: %+v", codexExact)
}
modelOnly, err := store.GetEnabledChannelsByModelAndProtocol(ctx, "gpt-4o", "")
if err != nil {
t.Fatalf("query empty protocol fallback: %v", err)
}
if len(modelOnly) != 1 || modelOnly[0].Name != "openai-native" {
t.Fatalf("unexpected model-only fallback result: %+v", modelOnly)
}
}
func TestConfig_LegacyProtocolTransformsHonorCurrentCapabilityMatrix(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
dbPath := filepath.Join(tmp, "legacy_invalid_protocol.db")
store, err := storage.CreateSQLiteStore(dbPath)
if err != nil {
t.Fatalf("create sqlite store: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
ctx := context.Background()
created, err := store.CreateConfig(ctx, &model.Config{
Name: "legacy-openai",
URL: "https://api.openai.com",
Priority: 10,
Enabled: true,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4o"},
},
})
if err != nil {
t.Fatalf("create config: %v", err)
}
if err := store.CreateAPIKeysBatch(ctx, []*model.APIKey{
{ChannelID: created.ID, KeyIndex: 0, APIKey: "sk-openai"},
}); err != nil {
t.Fatalf("create api key: %v", err)
}
rawDB, err := sql.Open("sqlite", "file:"+dbPath)
if err != nil {
t.Fatalf("open raw sqlite db: %v", err)
}
t.Cleanup(func() { _ = rawDB.Close() })
if _, err := rawDB.ExecContext(ctx,
`INSERT INTO channel_protocol_transforms(channel_id, protocol) VALUES (?, ?)`,
created.ID, "gemini",
); err != nil {
t.Fatalf("insert legacy supported transform: %v", err)
}
got, err := store.GetConfig(ctx, created.ID)
if err != nil {
t.Fatalf("get config: %v", err)
}
if len(got.ProtocolTransforms) != 1 || got.ProtocolTransforms[0] != "gemini" {
t.Fatalf("expected legacy gemini transform to remain loadable, got %#v", got.ProtocolTransforms)
}
geminiChannels, err := store.GetEnabledChannelsByExposedProtocol(ctx, "gemini")
if err != nil {
t.Fatalf("get gemini exposed channels: %v", err)
}
if len(geminiChannels) != 1 || geminiChannels[0].ID != created.ID {
t.Fatalf("expected legacy gemini transform to expose channel, got %+v", geminiChannels)
}
modelAndProtocol, err := store.GetEnabledChannelsByModelAndProtocol(ctx, "gpt-4o", "gemini")
if err != nil {
t.Fatalf("query model+protocol: %v", err)
}
if len(modelAndProtocol) != 1 || modelAndProtocol[0].ID != created.ID {
t.Fatalf("expected legacy gemini transform row to participate in model+protocol query, got %+v", modelAndProtocol)
}
}
func TestConfig_BatchUpdatePriority(t *testing.T) {
t.Parallel()
store := newTestStore(t, "priority.db")
ctx := context.Background()
// 创建多个渠道
var ids []int64
for i := 1; i <= 3; i++ {
cfg := &model.Config{
Name: "channel-" + string(rune('A'+i-1)),
URL: "https://api.example.com",
Priority: i,
Enabled: true,
}
created, err := store.CreateConfig(ctx, cfg)
if err != nil {
t.Fatalf("create config %d: %v", i, err)
}
ids = append(ids, created.ID)
}
// 批量更新优先级
updates := []struct {
ID int64
Priority int
}{
{ids[0], 100},
{ids[1], 200},
{ids[2], 300},
}
if _, err := store.BatchUpdatePriority(ctx, updates); err != nil {
t.Fatalf("batch update priority: %v", err)
}
// 验证更新
for i, id := range ids {
got, err := store.GetConfig(ctx, id)
if err != nil {
t.Fatalf("get config %d: %v", i, err)
}
expected := (i + 1) * 100
if got.Priority != expected {
t.Errorf("config %d priority: got %d, want %d", i, got.Priority, expected)
}
}
}
func TestConfig_ModelRedirect(t *testing.T) {
t.Parallel()
store := newTestStore(t, "redirect.db")
ctx := context.Background()
// 创建带模型重定向的渠道
cfg := &model.Config{
Name: "redirect-channel",
URL: "https://api.example.com",
Priority: 10,
Enabled: true,
ChannelType: "openai",
ModelEntries: []model.ModelEntry{
{Model: "gpt-4", RedirectModel: "gpt-4-turbo"},
{Model: "gpt-3.5-turbo"},
},
}
created, err := store.CreateConfig(ctx, cfg)
if err != nil {
t.Fatalf("create config: %v", err)
}
// 验证模型重定向被保存
got, err := store.GetConfig(ctx, created.ID)
if err != nil {
t.Fatalf("get config: %v", err)
}
var foundRedirect bool
for _, entry := range got.ModelEntries {
if entry.Model == "gpt-4" && entry.RedirectModel == "gpt-4-turbo" {
foundRedirect = true
break
}
}
if !foundRedirect {
t.Error("expected to find gpt-4 -> gpt-4-turbo redirect")
}
}