package core import ( "strings" "testing" ) // 渠道端点判据 —— 钉住"base_url 不吞 endpoint 路径"这条铁律。 // // 为什么需要这个测试 // ------------------ // 内核的 URL 是 `base_url + endpoint`(provider.go ChatURL)。把路径**同时** // 写进 base_url 和 endpoint,拼出来就是重复路径。生产实测两次撞上同一族缺陷: // // zcode base_url=.../api/v1/zcode-plan/anthropic // endpoint=/api/v1/zcode-plan/anthropic/v1/messages // ⇒ .../anthropic/api/v1/zcode-plan/anthropic/v1/messages → 404 // // minimax base_url=https://agent.minimax.cn + adapter=openai(默认 endpoint // /chat/completions)⇒ 打到 Web 前端首页,404「简单指令,无限可能」 // // 两者报错都是 404,但根因不同(路径重复 / 缺路径段)。单条断言咬不住两种, // 所以下面分三条断言。 // platformDef 从**真实加载的插件**里读一个平台定义。 // // ★ 必须走真实加载而不是正则解析源码:PLATFORMS 是 Lua 表,源里有注释、 // 条件拼装、以及文件末尾的绑定循环(refresh_fn / claim_fn / headers_fn 都在 // 那里挂)。拿正则在源文本上判断"有没有 refresh 段"会被注释里的字样骗 —— // 这个坑我上一轮差点踩进去。探针返回的是插件运行时看到的真相。 func platformDef(t *testing.T, id string) map[string]any { t.Helper() _, ps := poolCore(t) out, err := ps.Call("channels", "__platform_def_probe", map[string]any{"id": id}) if err != nil { t.Fatalf("__platform_def_probe(%s) 失败: %v", id, err) } // Plugins.Call 返回 {json:{...}, status:...} 包装(历史约定)。 wrapped, ok := out.(map[string]any) if !ok { t.Fatalf("探针返回不是表: %T", out) } if e, ok := wrapped["error"].(string); ok && e != "" { t.Fatalf("%s: %s", id, e) } inner, ok := wrapped["json"].(map[string]any) if !ok { t.Fatalf("探针返回缺 json 包装: %#v", wrapped) } return inner } // TestChannelBaseURLDoesNotSwallowEndpoint 是最核心的一条: // base_url 里**不得**出现 endpoint 也有的路径段。 // // 判别性:zcode 缺陷时 base_url 尾段是 "anthropic",endpoint 里也含 // "/anthropic/" ⇒ 命中。修好后 base_url 只到 host,永不命中。 func TestChannelBaseURLDoesNotSwallowEndpoint(t *testing.T) { for _, id := range []string{"zcode", "minimax", "codearts", "trae", "lobsterai", "buddy"} { t.Run(id, func(t *testing.T) { p := platformDef(t, id) base, _ := p["base_url"].(string) ep, _ := p["endpoint"].(string) base = strings.TrimRight(base, "/") if base == "" { t.Fatalf("%s 没有 base_url", id) } // 从 base_url 里取出路径部分(host 之后),逐段看是否也出现在 endpoint 里。 pathPart := "" if i := strings.Index(base, "://"); i >= 0 { if j := strings.Index(base[i+3:], "/"); j >= 0 { pathPart = base[i+3+j:] } } if pathPart == "" { return // 只到 host,天然不会重复 } for _, seg := range strings.Split(strings.Trim(pathPart, "/"), "/") { if seg == "" { continue } if strings.Contains(ep, "/"+seg+"/") || strings.HasSuffix(ep, "/"+seg) { t.Fatalf("%s: base_url 已含路径段 %q(base_url=%s),"+ "endpoint 里又出现(endpoint=%s)⇒ 内核拼 base_url+endpoint 时路径重复", id, seg, base, ep) } } }) } } // TestMinimaxUsesAnthropicProtocolAndRealInferPath 钉住 minimax 的两层缺陷。 // // 参考 channel-pack/src/minimax-product.ts: // // MINIMAX_INFER_PATH = '/mavis/api/v1/llm/v1/messages' ← Anthropic Messages // apiHost = 'https://agent.minimax.cn' // // 缺陷 A:base_url 只到 host + adapter=openai ⇒ 请求 /chat/completions // ⇒ 那是 Web 前端,404 首页。实测(2026-10-19): // /mavis/api/v1/llm/v1/messages → 401 invalid access token(路径对) // 裸 /chat/completions → 404 page not found // /mavis/api/v1/chat/completions → 404 // 缺陷 B:即使路径改对,openai 适配器的**请求体形状**也不对 // (minimax 推理端点是 Anthropic 协议)。 func TestMinimaxUsesAnthropicProtocolAndRealInferPath(t *testing.T) { p := platformDef(t, "minimax") ep, _ := p["endpoint"].(string) adapter, _ := p["adapter"].(string) if !strings.HasSuffix(ep, "/mavis/api/v1/llm/v1/messages") { t.Errorf("minimax endpoint = %q,应为 /mavis/api/v1/llm/v1/messages"+ "(照参考 MINIMAX_INFER_PATH);否则请求会打到 Web 前端", ep) } if adapter != "anthropic" { t.Errorf("minimax adapter = %q,应为 anthropic —— 推理端点是 Anthropic "+ "Messages 协议,用 openai 适配器会把请求体拼错", adapter) } } // TestCodeartsDeclaresRefreshHook 钉住"声明了可续期就必须声明怎么续"。 // // 真实缺陷(2026-10-19 用户报「华为云源用不了」): // codearts 的 security_token 只活 1 小时;account_refresh 走 refreshCredential(), // 它要求 plat.refresh.url 或 plat.refresh_fn 存在。当时只声明了 refreshable=true // (那是 accountView 里硬编码的期望),**没声明怎么续** ⇒ 恒出 // 「该平台未声明续期方式」,自动续期链空转,日志反复 `模型刷新失败 HTTP 400`。 // // 为什么不能只看 refreshable:那是"这个账号的凭据类型可续",与"平台实现了 // 续期"是两件事。前者为真而后者缺失,正是这个 bug 的形状。 func TestCodeartsDeclaresRefreshHook(t *testing.T) { p := platformDef(t, "codearts") hasHook, _ := p["has_refresh_fn"].(bool) hasDecl, _ := p["has_refresh_decl"].(bool) if !hasHook && !hasDecl { t.Fatal("codearts 既无 refresh_fn 也无 refresh.url —— " + "account_refresh 会恒返回「该平台未声明续期方式」," + "而 STS token 只有 1 小时寿命,过期后源的每个请求都会 400") } // 华为云 STS 要 DPoP 头,声明式 style 拿不到私钥 ⇒ 必须是钩子 if !hasHook { t.Error("codearts 只有声明式 refresh.url,但华为云 STS 需要 DPoP 头" + "(jwk 在凭据里),声明式 style 发不出来 ⇒ 必须用 refresh_fn") } }