You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
9.8 KiB
275 lines
9.8 KiB
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}
|
|
// 不经 HTTP 的例外:中国移动灵犀走客户端原生 SDK(agent_service 插件),地址在 SDK 内部,
|
|
// 这条模板只承载 AgentId/产品密钥/AI 球跳转开关。
|
|
noHTTP := map[string]bool{"llm_mobile_elf": true}
|
|
for _, tpl := range builtinSvcTemplates() {
|
|
if tpl.Category != svcCatLLM && tpl.Category != svcCatLLMVision {
|
|
continue
|
|
}
|
|
if noHTTP[tpl.Id] {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 科大讯飞 AIUI 的鉴权自成一套:X-CheckSum = MD5(api_key + X-CurTime + X-Param)。
|
|
// 两处一旦被「顺手统一」就会静默出问题:api_key 若被改成不加密,密钥会明文躺在库里并随
|
|
// thirdsvcs 原样下发;接口地址若被改成别家那种 https 形态,AIUI 的流式交互握不上手。
|
|
func TestIflytekAIUITemplate(t *testing.T) {
|
|
var tpl *ThirdSvcTemplate
|
|
for i, tp := range builtinSvcTemplates() {
|
|
if tp.Id == "sts_iflytek_aiui" {
|
|
tpl = &builtinSvcTemplates()[i]
|
|
break
|
|
}
|
|
}
|
|
if tpl == nil {
|
|
t.Fatal("内置模板里没有 sts_iflytek_aiui")
|
|
}
|
|
if tpl.Category != svcCatSTS {
|
|
t.Errorf("AIUI 应归 STS(端到端语音对话)=%d,实际 %d——它吐的是答复不是转写,归 STT 会被会议/翻译那些按类别选路的地方选错",
|
|
svcCatSTS, tpl.Category)
|
|
}
|
|
fields := map[string]SvcField{}
|
|
for _, f := range tpl.Fields {
|
|
fields[f.Key] = f
|
|
}
|
|
for _, k := range []string{"app_id", "api_key", "ws_url", "scene"} {
|
|
if _, ok := fields[k]; !ok {
|
|
t.Errorf("AIUI 模板缺字段 %s", k)
|
|
}
|
|
}
|
|
if !fields["api_key"].Encrypted {
|
|
t.Error("AIUI 的 api_key 必须加密存储:它是算 X-CheckSum 的密钥,明文等于谁拿到配置谁就能冒充本应用调用")
|
|
}
|
|
if fields["app_id"].Encrypted {
|
|
t.Error("AIUI 的 app_id 是请求头 X-Appid,不是密钥,不该加密——加密了端侧拿到的是密文")
|
|
}
|
|
if got := fields["ws_url"].DefValue; got != "wss://wsapi.xfyun.cn/v1/aiui" {
|
|
t.Errorf("AIUI 默认接口地址应为 wss://wsapi.xfyun.cn/v1/aiui,实际 %q", got)
|
|
}
|
|
}
|
|
|
|
// 废弃字段在**实例**上只删空的、留有值的。
|
|
// 背景:ast_alibaba 模板原先照云账号 AK/SK 建,而这条链路(百炼 qwen3.5-livetranslate)
|
|
// 用的是 app_key。那两个空的加密字段会让服务被判「缺凭据」且保存被拒——运营改不动。
|
|
func TestRemoveDeprecatedEmptyFields(t *testing.T) {
|
|
t.Run("空的废弃字段删掉", func(t *testing.T) {
|
|
out, removed := removeDeprecatedEmptyFields("ast_alibaba", []SvcField{
|
|
{Key: "app_key", DefValue: "sk-x", Encrypted: true},
|
|
{Key: "access_key_id", DefValue: "", Encrypted: true},
|
|
{Key: "access_key_secret", DefValue: " ", Encrypted: true},
|
|
})
|
|
if len(out) != 1 || out[0].Key != "app_key" {
|
|
t.Fatalf("只该留下 app_key,实际 %+v", out)
|
|
}
|
|
if len(removed) != 2 {
|
|
t.Fatalf("应删两个,实际 %v", removed)
|
|
}
|
|
})
|
|
t.Run("有值的废弃字段保留", func(t *testing.T) {
|
|
out, removed := removeDeprecatedEmptyFields("ast_alibaba", []SvcField{
|
|
{Key: "access_key_id", DefValue: "LTAI-real", Encrypted: true},
|
|
})
|
|
if len(out) != 1 || len(removed) != 0 {
|
|
t.Fatal("运营填过值的字段不能删——那是不可逆的数据")
|
|
}
|
|
})
|
|
t.Run("没声明废弃的模板不受影响", func(t *testing.T) {
|
|
in := []SvcField{{Key: "whatever", DefValue: ""}}
|
|
out, removed := removeDeprecatedEmptyFields("llm_qwen", in)
|
|
if len(out) != 1 || len(removed) != 0 {
|
|
t.Fatal("未声明废弃字段的模板不该动任何字段")
|
|
}
|
|
})
|
|
}
|
|
|