// models_test.go -- 模型角色系统的单元测试. // // 覆盖场景: // - NewModelRegistry 默认初始化 // - SetRole / GetRole 角色映射 // - Register 自定义模型注册 // - GetConfig / GetConfigForRole 配置获取 // - ContextWindow 上下文窗口查询 // - EstimateCost 成本计算 // - EstimateSimpleCost 简化成本计算 // - AllModels 模型列表 // - 并发安全性 package config import ( "strings" "sync" "testing" ) // testRegistryWithSonnet 创建包含一个测试模型的 registry(供需要模型信息的测试使用). func testRegistryWithSonnet() *ModelRegistry { r := NewModelRegistry() r.Register("claude-sonnet-4-6", &ModelConfig{ ID: "claude-sonnet-4-6", ContextWindow: 200000, MaxOutputTokens: 16384, InputPricePer1M: 3.0, OutputPricePer1M: 15.0, CacheReadPricePer1M: 0.3, CacheWritePricePer1M: 3.75, SupportsCaching: true, SupportsThinking: true, SupportsVision: true, }) return r } // TestNewModelRegistry 测试默认初始化 func TestNewModelRegistry(t *testing.T) { r := NewModelRegistry() // G9-C: DefaultRoles 为空--引擎不预设任何角色映射. // 所有角色应返回空字符串. if r.GetRole(RoleMain) != "" { t.Errorf("DefaultRoles 为空时 RoleMain 应为空字符串, 实际: %q", r.GetRole(RoleMain)) } if r.GetRole(RoleFast) != "" { t.Errorf("DefaultRoles 为空时 RoleFast 应为空字符串, 实际: %q", r.GetRole(RoleFast)) } if r.GetRole(RoleThinking) != "" { t.Errorf("DefaultRoles 为空时 RoleThinking 应为空字符串, 实际: %q", r.GetRole(RoleThinking)) } // 未映射的角色返回空 if r.GetRole(RoleEmbed) != "" { t.Errorf("未映射角色应返回空字符串, 实际: %q", r.GetRole(RoleEmbed)) } // DefaultModels 已清空,新建的 registry 无预注册模型 cfg := r.GetConfig("claude-sonnet-4-6") if cfg != nil { t.Error("DefaultModels 已清空,不应有预注册模型") } } // TestModelRegistry_SetRole 测试角色设置 func TestModelRegistry_SetRole(t *testing.T) { r := NewModelRegistry() // 修改 RoleMain r.SetRole(RoleMain, "claude-opus-4-6") if r.GetRole(RoleMain) != "claude-opus-4-6" { t.Errorf("设置后 RoleMain 应为 claude-opus-4-6, 实际: %q", r.GetRole(RoleMain)) } // 设置未注册的模型 ID(允许) r.SetRole(RoleEmbed, "custom-embed-model") if r.GetRole(RoleEmbed) != "custom-embed-model" { t.Errorf("设置后 RoleEmbed 应为 custom-embed-model, 实际: %q", r.GetRole(RoleEmbed)) } } // TestModelRegistry_Register 测试自定义模型注册 func TestModelRegistry_Register(t *testing.T) { r := NewModelRegistry() // 注册自定义模型 r.Register("gpt-4-turbo", &ModelConfig{ ContextWindow: 128000, MaxOutputTokens: 4096, InputPricePer1M: 10.0, OutputPricePer1M: 30.0, }) cfg := r.GetConfig("gpt-4-turbo") if cfg == nil { t.Fatal("自定义模型应已注册") } if cfg.ID != "gpt-4-turbo" { t.Errorf("模型 ID 应为 gpt-4-turbo, 实际: %q", cfg.ID) } if cfg.ContextWindow != 128000 { t.Errorf("上下文窗口应为 128000, 实际: %d", cfg.ContextWindow) } // 覆盖已有模型 r.Register("claude-sonnet-4-6", &ModelConfig{ ContextWindow: 300000, InputPricePer1M: 2.0, OutputPricePer1M: 10.0, }) cfg = r.GetConfig("claude-sonnet-4-6") if cfg.ContextWindow != 300000 { t.Errorf("覆盖后上下文窗口应为 300000, 实际: %d", cfg.ContextWindow) } } // TestModelRegistry_GetConfigForRole 测试角色配置获取 func TestModelRegistry_GetConfigForRole(t *testing.T) { r := NewModelRegistry() // G9-C: DefaultRoles 为空,所有角色默认未映射 cfg := r.GetConfigForRole(RoleMain) if cfg != nil { t.Error("DefaultRoles 为空时 RoleMain 配置应为 nil") } // 未映射的角色 cfg = r.GetConfigForRole(RoleEmbed) if cfg != nil { t.Error("未映射角色的配置应为 nil") } // 手动注册模型 + 设置角色后应能获取配置 r.Register("claude-sonnet-4-6", &ModelConfig{ID: "claude-sonnet-4-6", ContextWindow: 200000}) r.SetRole(RoleMain, "claude-sonnet-4-6") cfg = r.GetConfigForRole(RoleMain) if cfg == nil { t.Fatal("设置角色后 RoleMain 配置不应为 nil") } if cfg.ID != "claude-sonnet-4-6" { t.Errorf("RoleMain 配置 ID 应为 claude-sonnet-4-6, 实际: %q", cfg.ID) } } // TestModelRegistry_ContextWindow 测试上下文窗口查询 func TestModelRegistry_ContextWindow(t *testing.T) { r := testRegistryWithSonnet() // 已注册模型 if r.ContextWindow("claude-sonnet-4-6") != 200000 { t.Errorf("claude-sonnet-4-6 窗口应为 200000") } // 未知模型返回默认值 if r.ContextWindow("unknown-model") != 200000 { t.Errorf("未知模型默认窗口应为 200000") } } // TestModelRegistry_EstimateCost 测试成本计算 func TestModelRegistry_EstimateCost(t *testing.T) { r := testRegistryWithSonnet() // Sonnet: input=3.0/M, output=15.0/M cost := r.EstimateCost("claude-sonnet-4-6", 1_000_000, 1_000_000, 0, 0) expected := 3.0 + 15.0 if cost != expected { t.Errorf("Sonnet 1M+1M token 成本应为 $%.2f, 实际: $%.6f", expected, cost) } // 包含缓存的成本计算 // cache_read=0.3/M, cache_write=3.75/M costWithCache := r.EstimateCost("claude-sonnet-4-6", 500_000, 100_000, 500_000, 100_000) expectedWithCache := float64(500_000)*3.0/1_000_000 + float64(100_000)*15.0/1_000_000 + float64(500_000)*0.3/1_000_000 + float64(100_000)*3.75/1_000_000 if costWithCache != expectedWithCache { t.Errorf("带缓存成本应为 $%.6f, 实际: $%.6f", expectedWithCache, costWithCache) } // 未注册模型返回 0(不猜测定价) costUnknown := r.EstimateCost("unknown-model", 1000, 500, 0, 0) if costUnknown != 0 { t.Errorf("未注册模型应返回 0 成本,实际: %f", costUnknown) } } // TestModelRegistry_EstimateSimpleCost 测试简化成本计算 func TestModelRegistry_EstimateSimpleCost(t *testing.T) { r := testRegistryWithSonnet() cost := r.EstimateSimpleCost("claude-sonnet-4-6", 1000, 500) if cost <= 0 { t.Error("成本应 > 0") } // 更多 token 应更贵 cost2 := r.EstimateSimpleCost("claude-sonnet-4-6", 10000, 500) if cost2 <= cost { t.Error("更多 token 应更贵") } } // TestModelRegistry_AllModels 测试模型列表 func TestModelRegistry_AllModels(t *testing.T) { r := NewModelRegistry() models := r.AllModels() if len(models) != 0 { t.Errorf("DefaultModels 已清空,新 registry 应有 0 个模型, 实际: %d", len(models)) } // 注册新模型后数量增加 r.Register("custom-model", &ModelConfig{ContextWindow: 100000}) models = r.AllModels() if len(models) != 1 { t.Errorf("注册后模型数量应为 1, 实际: %d", len(models)) } } // TestModelRegistry_String 测试可读字符串 func TestModelRegistry_String(t *testing.T) { r := NewModelRegistry() s := r.String() // G9-C: DefaultRoles 为空,角色值应为空字符串 if !strings.Contains(s, "main=") { t.Errorf("String() 应包含 main= 前缀, 实际: %q", s) } // 设置角色后应反映新值 r.SetRole(RoleMain, "gpt-4o") r.SetRole(RoleFast, "gpt-4o-mini") s = r.String() if !strings.Contains(s, "main=gpt-4o") { t.Errorf("String() 应包含 main=gpt-4o, 实际: %q", s) } if !strings.Contains(s, "fast=gpt-4o-mini") { t.Errorf("String() 应包含 fast=gpt-4o-mini, 实际: %q", s) } } // TestModelRegistry_DeepCopy 测试注册后修改不影响已注册值 func TestModelRegistry_DeepCopy(t *testing.T) { r := NewModelRegistry() // 注册一个模型 original := &ModelConfig{ContextWindow: 200000, InputPricePer1M: 3.0} r.Register("test-model", original) // 修改原始指针不应影响 registry 中的拷贝 original.ContextWindow = 999999 cfg := r.GetConfig("test-model") if cfg.ContextWindow != 200000 { t.Error("修改原始指针不应影响 registry 中的值") } } // TestModelRegistry_GetConfigNil 测试获取不存在的模型 func TestModelRegistry_GetConfigNil(t *testing.T) { r := NewModelRegistry() cfg := r.GetConfig("nonexistent-model") if cfg != nil { t.Error("不存在的模型应返回 nil") } } // TestModelRegistry_ConcurrentAccess 测试并发安全性 func TestModelRegistry_ConcurrentAccess(t *testing.T) { r := NewModelRegistry() var wg sync.WaitGroup // 并发读写 for i := 0; i < 100; i++ { wg.Add(3) go func() { defer wg.Done() r.GetRole(RoleMain) }() go func() { defer wg.Done() r.GetConfig("claude-sonnet-4-6") }() go func() { defer wg.Done() r.EstimateCost("claude-sonnet-4-6", 1000, 500, 0, 0) }() } wg.Wait() } // TestDefaultModels_Empty 验证 DefaultModels 已清空(模型信息由各 provider 注册) func TestDefaultModels_Empty(t *testing.T) { if len(DefaultModels) != 0 { t.Errorf("DefaultModels 应为空 map, 实际有 %d 个模型", len(DefaultModels)) } } // TestDefaultRoles_Empty 验证 DefaultRoles 已清空(角色由消费者配置) func TestDefaultRoles_Empty(t *testing.T) { if len(DefaultRoles) != 0 { t.Errorf("DefaultRoles 应为空 map, 实际有 %d 个角色", len(DefaultRoles)) } }