package config import ( "strings" "testing" ) // ★ P2:源名允许字符集契约 —— 仅 `[A-Za-z0-9_-]`,**不得含 `:` 与 `/`**。 // // 为何必须收窄(原因写在这里,因为 docs 会被改、判据不会): // // 模型寻址支持 `源名:模型名` / `源名/模型名` / `源名-模型名` 三种限定写法 // (WebUI 下拉框用 `源名:模型名` 作 option value:本实例 44 个模型被多个源 // 同时提供,必须能指定「从哪个源调」)。剥离源名的那一步是 // `internal/provider/splitPinned`:先做**最长源名前缀匹配**,再要求源名后 // 紧跟一个分隔符。 // // ⇒ 若源名**自己含** `:` 或 `/`,那个字符同时是「源名的一部分」和「分隔符」, // 限定名就成了歧义串: // // src="a:b" model="m" → "a:b:m" → ("a", "b:m") 还是 ("a:b", "m")? // src="a/b" model="m" → "a/b/m" → ("a", "b/m") 还是 ("a/b", "m")? // // 两种切法都自圆其说,结果取决于**当前恰好有哪些源名** —— 同一客户端请求串 // 在不同部署/不同启动顺序下可能落到不同源,而且**不会报错**(splitPinned // 会命中某一个分支然后路由到别处)。历史缺陷正是这一族:`IndexAny(model, // "-:/")` 按第一个分隔符切,把 `channels-trae-xxx:model` 切成 src="channels" // ⇒ 永不匹配 ⇒ `model ... is not configured`。 // // 注意 `-` 是**允许**的:本仓渠道源就是 `channels-<平台>-<账号id>`, // 分隔符在源名之后,不产生歧义。 func TestValidateSourceNameAllowsContractCharset(t *testing.T) { allowed := []string{ "channels-trae-b8dd6b3df30e", // 真实渠道源形态(含两个 -) "channels-buddy-buddy-main", "DeepSeek", "my_source_1", "a", "-leading-dash", "trailing-dash-", "123456", } for _, name := range allowed { if err := ValidateSourceName(name); err != nil { t.Errorf("★ 合法源名 %q 被拒: %v", name, err) } } } // 反例:`:` 与 `/` 必须**被点名拒绝**(而不是泛泛地说"非法字符")—— // 运维看到错误信息才知道这是限定名分隔符冲突,不是手滑。 func TestValidateSourceNameRejectsPinningSeparators(t *testing.T) { cases := []string{ "a:b", "vendor:model", "a/b", "openai/gpt", "channels-trae-b8dd6b3df30e:glm-5.2", // 把"限定名"当源名写进来 "src/sub:name", } for _, name := range cases { err := ValidateSourceName(name) if err == nil { t.Fatalf("★ 源名 %q 含限定名分隔符,必须被拒(否则 `源名:模型名` 歧义)", name) } msg := err.Error() if !strings.Contains(msg, "pinning separator") { t.Errorf("错误信息应点名「pinning separator」,实得: %v", err) } if !strings.Contains(msg, name) { t.Errorf("错误信息应回显源名 %q,实得: %v", name, err) } } } // 其它非法字符同样拒(防止 `源 名`、`源名\n` 这种把日志/URL 搞乱的写法)。 func TestValidateSourceNameRejectsOtherIllegalChars(t *testing.T) { for _, name := range []string{"has space", "tab\there", "new\nline", "中文名", "dot.name", "plus+name", "@at"} { if err := ValidateSourceName(name); err == nil { t.Errorf("★ 源名 %q 含非法字符,必须被拒(允许集只有 %s)", name, SourceNameAllowedPattern) } } if err := ValidateSourceName(""); err == nil { t.Error("空源名必须被拒") } } // 存量兼容:启动时**只告警不报错**。 // // 为什么不能直接报错:存量配置里可能已经有带 `:` 的源名,升级瞬间让网关 // 起不来(比"限定名歧义"更坏 —— 整个服务不可用)。契约从**新数据**开始强制 // (core.normalizeSource 硬拒),老数据给一条可读告警。 func TestSourceNameWarningIsSoftForLegacyConfig(t *testing.T) { cfg := Config{Sources: []Source{ // 存量形态:带 `:` —— 必须能启动(不返回 error),但要被点名告警 {Name: "legacy:v1", BaseURL: "http://x", Models: []Model{{ID: "m1"}}}, }} if err := cfg.ApplyDefaults(); err != nil { t.Fatalf("★ 存量配置带 `:` 不得导致启动失败(会把整个网关 brick 掉): %v", err) } if w := SourceNameWarning("legacy:v1"); w == "" { t.Fatal("★ 存量非法名必须给出告警文案(否则问题彻底静默)") } if w := SourceNameWarning("channels-trae-abc"); w != "" { t.Fatalf("合法源名不该产生告警,实得 %q", w) } }