package lua import ( "strings" "testing" ) // ★ P1-1:anthropic 基座必须发 `x-api-key`,不是 `Authorization: Bearer`。 // // 真实缺陷:anthropic.lua 只有一个静态 `anthropic-version`、**没有** // build_headers ⇒ 内核走兜底 `Authorization: Bearer ` ⇒ Anthropic // 协议一律回 401 `invalid x-api-key`。继承该基座的渠道源(channels-zcode) // 一有账号就会立刻暴露,与 trae 修复前完全同型(静态头 + 内核兜底 Bearer, // 缺的正是协议要的那一个头)。 // // 判据咬的是**真实产出表**(vm.BuildHeaders 就是内核发请求前走的那条路径), // 不造 HTTP 假上游 —— 假上游需要 URL 覆盖/凭据 ref/路径匹配全部对齐,任一环 // 错都会让「没发请求」伪装成「头不对」(plan §3 第 4 条踩过三轮)。 func TestAnthropicBuildHeadersSendsXAPIKey(t *testing.T) { vm := NewVM(freshAdapterDir(t)) if err := vm.Start(); err != nil { t.Fatal(err) } defer vm.Stop() if !vm.HasFn("anthropic", "build_headers") { t.Fatal("★ anthropic 基座没有 build_headers —— 内核会兜底发 " + "Authorization: Bearer,而 Anthropic 协议要 x-api-key ⇒ 404/401") } hdrs, err := vm.BuildHeaders("anthropic", map[string]interface{}{ "api_key": "sk-ant-KEY-123", "url": "https://api.anthropic.com/v1/messages", "method": "POST", "body": `{"model":"claude"}`, }) if err != nil { t.Fatalf("BuildHeaders: %v", err) } // ① 键集合:必须**恰好**是这两个。多出来的 Authorization 会让上游面对 // 两套鉴权方案(部分 Anthropic 兼容上游对歧义鉴权直接拒)。 if len(hdrs) != 2 { t.Fatalf("★ 头集合应恰好 2 个(x-api-key + anthropic-version),实得 %d: %v", len(hdrs), keysOf(hdrs)) } if got := hdrs["x-api-key"]; got != "sk-ant-KEY-123" { t.Fatalf("★ x-api-key 必须取凭据原值,实得 %q(头: %v)", got, keysOf(hdrs)) } if got := hdrs["anthropic-version"]; got != "2023-06-01" { t.Fatalf("anthropic-version 应为 2023-06-01,实得 %q", got) } if _, has := hdrs["Authorization"]; has { t.Fatalf("★ 不得同时发 Authorization(与 x-api-key 冲突,上游视为歧义): %v", hdrs) } // ② 值形状:不能带 Bearer 前缀(x-api-key 的值是裸 key) if strings.Contains(hdrs["x-api-key"], "Bearer") { t.Fatalf("★ x-api-key 的值是**裸 key**,不能带 Bearer 前缀,实得 %q", hdrs["x-api-key"]) } // ③ key 绝不外泄到别的头里(防止某次改动把 key 顺手塞进 UA 之类) for k, v := range hdrs { if k != "x-api-key" && strings.Contains(v, "sk-ant-KEY-123") { t.Fatalf("★ 凭据只能出现在 x-api-key,泄露到了 %s=%q", k, v) } } t.Logf("✓ anthropic 头: %v", keysOf(hdrs)) } // 配对判据:没有凭据时仍要发 anthropic-version(协议头),但**不得**发空 // x-api-key —— 空值头会让上游回 `invalid x-api-key` 而不是清晰的"未配置凭据"。 func TestAnthropicBuildHeadersWithoutCredential(t *testing.T) { vm := NewVM(freshAdapterDir(t)) if err := vm.Start(); err != nil { t.Fatal(err) } defer vm.Stop() for _, meta := range []map[string]interface{}{nil, {}, {"api_key": ""}} { hdrs, err := vm.BuildHeaders("anthropic", meta) if err != nil { t.Fatalf("BuildHeaders(%v): %v", meta, err) } if hdrs["anthropic-version"] != "2023-06-01" { t.Fatalf("mate=%v 时仍须有 anthropic-version,实得 %v", meta, hdrs) } if _, has := hdrs["x-api-key"]; has { t.Fatalf("meta=%v 时不得发空 x-api-key,实得 %v", meta, hdrs) } } } // 兜底路径:有些调用方只把 key 放在 source.meta.api_key(kimicode 同约定)。 // 基座必须与 kimicode 一致地认这第二种位置,否则换调用方就静默少头。 func TestAnthropicBuildHeadersReadsSourceMetaKey(t *testing.T) { vm := NewVM(freshAdapterDir(t)) if err := vm.Start(); err != nil { t.Fatal(err) } defer vm.Stop() hdrs, err := vm.BuildHeaders("anthropic", map[string]interface{}{ "source": map[string]interface{}{ "name": "claude-direct", "meta": map[string]interface{}{"api_key": "KEY-FROM-META"}, }, }) if err != nil { t.Fatalf("BuildHeaders: %v", err) } if hdrs["x-api-key"] != "KEY-FROM-META" { t.Fatalf("★ 应读 source.meta.api_key 兜底,实得 %q(头: %v)", hdrs["x-api-key"], keysOf(hdrs)) } } func keysOf(m map[string]string) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } return out }