package core import ( "encoding/json" "os" "path/filepath" "strings" "testing" "llmsproxy/internal/lua" ) // ★ ZCode 的身份块是 3012 准入的**唯一开关**。 // // 参考实测(三个账号、17 次请求):system 缺官方身份块结构时,上游直接 // // HTTP 405 + {"code":3012,"msg":"...unusual activity."} // // 且与 HTTP 头、版本号、请求频率、多轮历史、tools 声明、运行时**全部无关**。 // // 判据矩阵(照参考,逐条锁定): // // 无 system → 3012 // 仅 cliPrefix(42 字符) → 3012 // cliPrefix + stable(约 2898 字符)→ 200 // // ⇒ 判据是「身份块**结构**是否存在」,不是字符数凑够。 // // 本判据看**适配器真正产出给上游的请求体**(transform_request 的返回值), // 而不是"适配器装了没"这类内部信号。 func TestZcodeAdapterInjectsOfficialIdentityBlocks(t *testing.T) { h := newZcodeIdentityHarness(t) out, err := h.transformRequest(`{"model":"glm-5.3-flash","messages":[{"role":"user","content":"hi"}]}`) if err != nil { t.Fatalf("transform_request: %v", err) } var req map[string]interface{} if err := json.Unmarshal([]byte(out), &req); err != nil { t.Fatalf("产出不是 JSON: %v\n%s", err, out) } // ① system 必须是**块数组**(官方形态),不是纯字符串 sys, ok := req["system"].([]interface{}) if !ok { t.Fatalf("system 必须是块数组(上游检查的是这个结构),实得 %T: %v", req["system"], req["system"]) } if len(sys) < 3 { t.Fatalf("system 块数至少 3(cliPrefix + stable + Environment),实得 %d", len(sys)) } // ② block[0] 必须是官方 CLI 前缀(准入必需) b0, _ := sys[0].(map[string]interface{}) if got, _ := b0["text"].(string); got != "You are ZCode, an interactive coding agent" { t.Fatalf("block[0] 必须是官方 CLI 前缀(逐字),实得 %q", got) } // ③ block[1] 必须是官方 stable 段(准入必需),长度对得上 b1, _ := sys[1].(map[string]interface{}) stable, _ := b1["text"].(string) if len(stable) < 2800 { t.Fatalf("block[1](stable 段)长度 %d < 2800 —— 身份块不完整会被判为裸请求(3012)", len(stable)) } for _, marker := range []string{ "You are an interactive ZCode agent", "# Harness", "# ZCode Desktop Context", } { if !strings.Contains(stable, marker) { t.Fatalf("stable 段缺 %q —— 身份块被截断/改写", marker) } } // ④ block[2] 是 Environment 段(官方每会话都发;不发则模型不知 cwd/平台) b2, _ := sys[2].(map[string]interface{}) env, _ := b2["text"].(string) if !strings.HasPrefix(env, "# Environment") { t.Fatalf("block[2] 必须是 Environment 段,实得 %q", env[:zcodeMin(40, len(env))]) } for _, marker := range []string{"Primary working directory", "Platform", "powered by the model named"} { if !strings.Contains(env, marker) { t.Fatalf("Environment 段缺 %q:%s", marker, env) } } // ⑤ 总长度必须达到参考声明的量级(2898) total := len("You are ZCode, an interactive coding agent") + len(stable) if total < 2800 { t.Fatalf("身份块合计 %d 字符 < 2800(参考实测的准入值约 2898)", total) } // ⑥ 日期块:插到第一条 user 消息的 content **数组**最前(官方形态) msgs, _ := req["messages"].([]interface{}) if len(msgs) == 0 { t.Fatal("messages 为空") } m0, _ := msgs[0].(map[string]interface{}) content, ok := m0["content"].([]interface{}) if !ok { t.Fatalf("首条 user 的 content 必须是**块数组**(拼进字符串会改变结构、仍被判裸请求),实得 %T", m0["content"]) } c0, _ := content[0].(map[string]interface{}) if got, _ := c0["text"].(string); !strings.Contains(got, "") { t.Fatalf("content[0] 必须是日期块(),实得 %q", got) } t.Logf("✓ 身份块注入正确:块数=%d stable=%d 合计=%d", len(sys), len(stable), total) } // ★ 调用方的 system 必须**追加在最后**:上游检查看的是**前缀结构**, // 官方身份块必须处在开头位置。若被前置会让身份块不在前缀而吃 3012。 func TestZcodeCallerSystemIsAppendedLast(t *testing.T) { h := newZcodeIdentityHarness(t) out, err := h.transformRequest( `{"model":"glm-5.3-flash","messages":[{"role":"system","content":"CALLER_PROMPT_XYZ"},{"role":"user","content":"hi"}]}`) if err != nil { t.Fatal(err) } var req map[string]interface{} if err := json.Unmarshal([]byte(out), &req); err != nil { t.Fatal(err) } sys, _ := req["system"].([]interface{}) if len(sys) < 4 { t.Fatalf("带调用方 system 时应为 4 块(3 官方 + 1 调用方),实得 %d", len(sys)) } last, _ := sys[len(sys)-1].(map[string]interface{}) lt, _ := last["text"].(string) if !strings.Contains(lt, "CALLER_PROMPT_XYZ") { t.Fatalf("调用方 system 必须在**最后一块**(上游看前缀结构;前置会让身份块不在前缀)") } // 且前三块仍是官方块(身份块没有被调用方内容挤走) b0, _ := sys[0].(map[string]interface{}) if got, _ := b0["text"].(string); got != "You are ZCode, an interactive coding agent" { t.Fatalf("block[0] 必须仍是官方 CLI 前缀,实得 %q", got) } } // 适配器自检出口:身份块长度与 anthropic 基座装载状态。 // 「装了但身份块是空的」这类问题(转义/截断/基座读不到)没有它只能靠猜。 func TestZcodeAdapterProbeReportsIdentityLength(t *testing.T) { h := newZcodeIdentityHarness(t) res, err := h.ps.Call("channels", "__adapter_install_probe", map[string]interface{}{}) if err != nil { t.Fatal(err) } j := res.(map[string]interface{})["json"].(map[string]interface{}) zc, ok := j["zcode"].(map[string]interface{}) if !ok { t.Fatalf("探针未报告 zcode 适配器装载结果: %v", j) } if zc["ok"] != true { t.Fatalf("zcode 适配器装载失败: %v", zc["err"]) } } // zcodeIdentityHarness:加载插件(它会**自装** channels-zcode 适配器), // 然后直接调那个适配器的 transform_request。 // // 直接调已装载的适配器(而不是自己去 load 源码)更接近真实路径:判据验的是 // 「上游实际收到的请求体」,包含前导注入、桥的装载等全部环节。 type zcodeIdentityHarness struct { t *testing.T c *Core ps *lua.Plugins } func newZcodeIdentityHarness(t *testing.T) *zcodeIdentityHarness { t.Helper() c := kernelTestCore(t) ps := c.Plugins() src, err := readBundledChannels(t) if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } return &zcodeIdentityHarness{t: t, c: c, ps: ps} } // transformRequest 调**已自装的** channels-zcode 适配器的 transform_request。 func (h *zcodeIdentityHarness) transformRequest(body string) (string, error) { h.t.Helper() vm := h.c.VM() if vm == nil { h.t.Fatal("VM 不可用") } return vm.Transform("channels-zcode", "transform_request", body) } func zcodeMin(a, b int) int { if a < b { return a } return b } // ★ 身份块注入不得破坏协议转换:工具调用、图像、流式帧仍必须走 anthropic 那一份。 // // 为什么必须验:本适配器的手法是把 anthropic.lua 的源码读进来执行、只覆盖 // transform_request。若「读不到基座」或「覆盖时丢了字段」,协议转换会静默退化 // —— 表现为工具调用参数丢失、图像块消失,而身份块看起来完全正常。 func TestZcodeAdapterKeepsAnthropicProtocolConversion(t *testing.T) { h := newZcodeIdentityHarness(t) t.Run("工具定义与工具调用存活", func(t *testing.T) { in := `{"model":"glm-5.3-flash","messages":[{"role":"user","content":"go"}],` + `"tools":[{"type":"function","function":{"name":"get_weather",` + `"description":"d","parameters":{"type":"object","properties":{"c":{"type":"string"}}}}}]}` out, err := h.transformRequest(in) if err != nil { t.Fatal(err) } // anthropic 形态:tools[].name / input_schema(顶层,不是嵌套在 function 里) if !strings.Contains(out, `"get_weather"`) { t.Fatalf("工具定义在转换中丢失(identity 注入不该影响它):%s", out) } if !strings.Contains(out, `"input_schema"`) { t.Fatalf("工具 schema 未转成 anthropic 形态(input_schema 缺失):%s", out) } }) t.Run("多模态图像块存活", func(t *testing.T) { in := `{"model":"glm-5.3-flash","messages":[{"role":"user","content":[` + `{"type":"text","text":"what"},` + `{"type":"image_url","image_url":{"url":"data:image/png;base64,AAAA"}}]}]}` out, err := h.transformRequest(in) if err != nil { t.Fatal(err) } if !strings.Contains(out, `"image"`) || !strings.Contains(out, `"base64"`) { t.Fatalf("图像块在转换中丢失:%s", out) } }) t.Run("基座装载状态可见", func(t *testing.T) { // 读适配器的自检出口:base_loaded 必须为 true(否则身份块虽在、 // 协议转换已退化为透传) res, err := h.ps.Call("channels", "__adapter_install_probe", map[string]interface{}{}) if err != nil { t.Fatal(err) } j := res.(map[string]interface{})["json"].(map[string]interface{}) zc := j["zcode"].(map[string]interface{}) if zc["ok"] != true { t.Fatalf("zcode 适配器未装载: %v", zc) } }) } // ★ 基座(anthropic.lua)读不到时必须**响亮失败**,不得静默退化。 // // 反例危害:若"尽力而为"(只注入身份块、不作协议转换),会产出一个身份块 // 看着正确的请求,而 tools/图像/流式结构全未转换 —— 上游可能 200,但模型 // 拿不到工具定义,症状是"模型变笨了",与准入毫不相干,排查会完全跑偏。 // // 判据看的是**适配器的真实产出**:把适配器目录指到空目录(基座不可得), // transform_request 必须返回错误而不是原样透传。 func TestZcodeAdapterFailsLoudlyWithoutAnthropicBase(t *testing.T) { // 隔离目录:插件目录下没有兄弟 adapters/ 目录 ⇒ 基座读不到 dir := t.TempDir() pluginDir := filepath.Join(dir, "plugins") if err := os.MkdirAll(pluginDir, 0755); err != nil { t.Fatal(err) } t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir) c := kernelTestCore(t) ps := c.Plugins() src, err := readBundledChannels(t) if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } vm := c.VM() if vm == nil { t.Fatal("VM 不可用") } // 插件自装时若基座读不到,适配器仍应装载(身份块是静态的), // 但调用必须报错 out, err := vm.Transform("channels-zcode", "transform_request", `{"model":"m","messages":[{"role":"user","content":"hi"}]}`) if err == nil { t.Fatalf("基座不可得时必须报错,却成功产出了:%s", out) } if !strings.Contains(err.Error(), "基座") { t.Fatalf("错误信息应点明基座问题(可定位),实得 %v", err) } t.Logf("✓ 基座缺失时响亮失败: %v", err) } // ★ 日期块必须**幂等**:已以 开头则不重复插。 // // 不判的后果:同一请求重试/多次 transform 时日期块叠成多层,改变 content 的 // 前缀结构(上游检查的正是结构)。参考 withContextPrefix 有 startsWithSystemReminder。 func TestZcodeContextPrefixIsIdempotent(t *testing.T) { h := newZcodeIdentityHarness(t) // 已是插入后的形态(首条 user 的 content[0] 就是 system-reminder) in := `{"model":"m","messages":[{"role":"user","content":[` + `{"type":"text","text":"\nOLD\n"},` + `{"type":"text","text":"hi"}]}]}` out, err := h.transformRequest(in) if err != nil { t.Fatal(err) } var req map[string]interface{} if err := json.Unmarshal([]byte(out), &req); err != nil { t.Fatal(err) } msgs := req["messages"].([]interface{}) content := msgs[0].(map[string]interface{})["content"].([]interface{}) n := 0 for _, c := range content { if b, ok := c.(map[string]interface{}); ok { if s, _ := b["text"].(string); strings.Contains(s, "") { n++ } } } if n != 1 { t.Fatalf("日期块必须幂等(已存在则不重复插),实得 %d 个 system-reminder 块", n) } } // ★ 日期块只处理**第一条**消息,且它必须 role == "user"。 // // 反例(我第一版就是这样):往后扫第一个 user 消息 —— 若首条是 assistant, // 官方行为是**不动**,而不是去找后面那条 user。判据钉住这个差别。 func TestZcodeContextPrefixOnlyTouchesFirstMessage(t *testing.T) { h := newZcodeIdentityHarness(t) t.Run("首条不是 user 时不动", func(t *testing.T) { in := `{"model":"m","messages":[` + `{"role":"assistant","content":"opening"},` + `{"role":"user","content":"hi"}]}` out, err := h.transformRequest(in) if err != nil { t.Fatal(err) } var req map[string]interface{} if err := json.Unmarshal([]byte(out), &req); err != nil { t.Fatal(err) } msgs := req["messages"].([]interface{}) // ⚠ 注意:文本会被 anthropic 基座转成**块数组**(协议转换的正常行为), // 所以这里不能断言 content 仍是字符串 —— 我第一版就这么断言,把正确的 // 实现判成了错。判据要验的是「**日期块**有没有被插进来」这个事实。 noReminder := func(content interface{}) bool { switch c := content.(type) { case string: return !strings.Contains(c, "") case []interface{}: for _, e := range c { if b, ok := e.(map[string]interface{}); ok { if t, _ := b["text"].(string); strings.Contains(t, "") { return false } } } } return true } // 首条(assistant)不该被插日期块 if !noReminder(msgs[0].(map[string]interface{})["content"]) { t.Fatal("首条是 assistant 时不该插日期块(官方只看第一条且要求 role=user)") } // 后面的 user 也**不该**被插(官方不往后找) if !noReminder(msgs[1].(map[string]interface{})["content"]) { t.Fatal("日期块被插到了后面的 user 消息上 —— 官方只看第一条") } // 且原文本内容必须保留(转换不能丢内容) b, _ := json.Marshal(msgs[0]) if !strings.Contains(string(b), "opening") { t.Fatal("首条 assistant 的内容丢了") } }) t.Run("首条是 user 时插入", func(t *testing.T) { in := `{"model":"m","messages":[{"role":"user","content":"hi"}]}` out, err := h.transformRequest(in) if err != nil { t.Fatal(err) } var req map[string]interface{} if err := json.Unmarshal([]byte(out), &req); err != nil { t.Fatal(err) } msgs := req["messages"].([]interface{}) content, ok := msgs[0].(map[string]interface{})["content"].([]interface{}) if !ok { t.Fatal("首条 user 的 content 应升级为块数组") } b0 := content[0].(map[string]interface{}) if s, _ := b0["text"].(string); !strings.Contains(s, "") { t.Fatalf("首条 user 的 content[0] 必须是日期块,实得 %q", s) } }) }