package console import "testing" // 遗留字段并入现行字段(callback → callback_path)的回归。 // 运行时只读 callback_path,老实例里两个并存会让后台出现重复行、旧的那个还是死值。 func TestMigrateAliasFields(t *testing.T) { fieldOf := func(svc *ThirdSvcConfig, key string) (SvcField, bool) { for _, f := range svc.Fields { if f.Key == key { return f, true } } return SvcField{}, false } t.Run("只有遗留字段:改名并补前导斜杠", func(t *testing.T) { svc := &ThirdSvcConfig{Id: "asr_ali", Fields: []SvcField{ {Key: "api_key", DefValue: "sk-x"}, {Key: "callback", DefValue: "api/home/echomeet_alibackcall"}, }} if len(migrateAliasFields(svc)) == 0 { t.Fatal("应报告有改动") } if _, ok := fieldOf(svc, "callback"); ok { t.Error("遗留字段必须删除") } f, ok := fieldOf(svc, "callback_path") if !ok { t.Fatal("现行字段必须存在") } if f.DefValue != "/api/home/echomeet_alibackcall" { t.Errorf("相对路径要补前导斜杠, got %q", f.DefValue) } }) t.Run("新字段已有值:以新值为准,只删旧的", func(t *testing.T) { svc := &ThirdSvcConfig{Id: "asr_byte", Fields: []SvcField{ {Key: "callback", DefValue: "/old/path"}, {Key: "callback_path", DefValue: "/api/home/echomeet_backcall"}, }} if len(migrateAliasFields(svc)) == 0 { t.Fatal("应报告有改动") } if _, ok := fieldOf(svc, "callback"); ok { t.Error("遗留字段必须删除") } f, _ := fieldOf(svc, "callback_path") if f.DefValue != "/api/home/echomeet_backcall" { t.Errorf("绝不能被旧值覆盖, got %q", f.DefValue) } }) t.Run("新字段存在但为空:吃下旧值", func(t *testing.T) { svc := &ThirdSvcConfig{Id: "asr_ali", Fields: []SvcField{ {Key: "callback", DefValue: "/api/home/echomeet_alibackcall"}, {Key: "callback_path", DefValue: ""}, }} migrateAliasFields(svc) f, _ := fieldOf(svc, "callback_path") if f.DefValue != "/api/home/echomeet_alibackcall" { t.Errorf("空的新字段应吃下旧值, got %q", f.DefValue) } }) t.Run("幂等:没有遗留字段时不动", func(t *testing.T) { svc := &ThirdSvcConfig{Id: "asr_ali", Fields: []SvcField{ {Key: "api_key", DefValue: "sk-x"}, {Key: "callback_path", DefValue: "/api/home/echomeet_alibackcall"}, }} if len(migrateAliasFields(svc)) > 0 { t.Error("无遗留字段时不该报告改动") } if len(svc.Fields) != 2 { t.Errorf("字段数不该变, got %d", len(svc.Fields)) } }) t.Run("旧字段是空值:直接丢弃,不产生空的新字段", func(t *testing.T) { svc := &ThirdSvcConfig{Id: "asr_ali", Fields: []SvcField{ {Key: "api_key", DefValue: "sk-x"}, {Key: "callback", DefValue: ""}, }} migrateAliasFields(svc) if _, ok := fieldOf(svc, "callback_path"); ok { t.Error("旧字段没值就不该凭空造出新字段(该字段由模板补齐负责,带正确默认值)") } if len(svc.Fields) != 1 { t.Errorf("只该剩 api_key, got %d 个", len(svc.Fields)) } }) } // 补字段只加不改:已有字段的值/描述一律不动,缺的按模板默认值补上。 func TestAppendMissingFields(t *testing.T) { have := []SvcField{ {Key: "api_key", Description: "我改过的描述", DefValue: "sk-x", Sort: 0}, } want := []SvcField{ {Key: "api_key", Description: "模板描述", DefValue: ""}, {Key: "workspace_id", Description: "工作空间 ID", DefValue: ""}, {Key: "languages", Description: "支持识别的语言", Type: "langs", DefValue: "zh-CN,vi-VN"}, } out, added := appendMissingFields(have, want) if len(added) != 2 { t.Fatalf("应补 2 个字段, got %v", added) } if out[0].Description != "我改过的描述" || out[0].DefValue != "sk-x" { t.Error("已有字段的描述和值都不能被模板覆盖") } var langs SvcField for _, f := range out { if f.Key == "languages" { langs = f } } // languages 留空=不限,比给默认值危险,所以补进去时必须带上模板默认值 if langs.DefValue != "zh-CN,vi-VN" { t.Errorf("补进来的 languages 要带模板默认值, got %q", langs.DefValue) } if langs.Type != "langs" { t.Errorf("字段类型要保留,否则前端渲染成普通输入框, got %q", langs.Type) } } // categories 是逗号分隔多值,匹配不能用子串(类别 1 不能命中 "10")。 func TestCategoriesHas(t *testing.T) { cases := []struct { categories string cat int32 want bool }{ {"8", 8, true}, {"5,8", 8, true}, {"8,9", 8, true}, {"5,8,9", 8, true}, {" 8 , 9 ", 8, true}, // 容忍空格 {"10", 1, false}, // 子串误命中 {"5,9", 8, false}, {"", 8, false}, } for _, c := range cases { if got := categoriesHas(c.categories, c.cat); got != c.want { t.Errorf("categoriesHas(%q, %d) = %v, want %v", c.categories, c.cat, got, c.want) } } } // 大模型类服务必须给出接口地址:客户端拿配置就得能直接发请求,缺地址等于下发了个半成品。 // llm_qwen 就漏过一次(同厂商的 llmv_qwen 一直有 base_url),客户端只能自己写死 dashscope 地址。 // 新增大模型模板时这条会先炸,而不是等客户端反馈「阿里的 LLM 没有 base_url」。 func TestLLMTemplatesCarryEndpoint(t *testing.T) { // 地址字段的几种写法:base_url(OpenAI 兼容) / endpoint(Google 自定义端点) addrKeys := map[string]bool{"base_url": true, "endpoint": true} for _, tpl := range builtinSvcTemplates() { if tpl.Category != svcCatLLM && tpl.Category != svcCatLLMVision { continue } ok := false for _, f := range tpl.Fields { if addrKeys[f.Key] { ok = true break } } if !ok { t.Errorf("大模型模板 %s(%s) 没有接口地址字段(base_url/endpoint),客户端不知道往哪发请求", tpl.Id, tpl.Name) } } } // 带默认值的地址字段不能写错——默认值是直接下发给客户端的,写错比不写更难查。 func TestQwenTemplateBaseURL(t *testing.T) { want := map[string]string{ "llm_qwen": "https://dashscope.aliyuncs.com/compatible-mode/v1", "llmv_qwen": "", // 视觉模板的 base_url 是可选项,不设默认值,只要求字段在 } for _, tpl := range builtinSvcTemplates() { exp, care := want[tpl.Id] if !care { continue } var got string found := false for _, f := range tpl.Fields { if f.Key == "base_url" { got, found = f.DefValue, true } } if !found { t.Fatalf("%s 缺 base_url 字段", tpl.Id) } if exp != "" && got != exp { t.Fatalf("%s 的 base_url 默认值应为 %s,实际 %q", tpl.Id, exp, got) } } }