package lua import ( "encoding/json" "os" "strings" "testing" ) // ★★ P3:夹具形状防回归 —— 钉住三类**真实上游形状**。 // // 本轮最大的教训不是某个 bug,而是「**夹具形状错 ⇒ 判据必然虚假通过**」: // trae 的 ExchangeToken 缺陷(只读顶层、不认 `Result` 信封)在两个版本的 // 扁平夹具 `{"Token":...}` 下**全部判据都是绿的**,真实登录却报「缺少 Token」。 // 也就是说:判据写得再严,只要它喂进去的形状不是生产形状,它就毫无意义。 // // 因此这里不只钉行为,还要钉**形状**本身,且钉在**真实的解析器/转换器**上 // (经插件探针调用同一份实现),而不是在 Go 侧重写一遍解包逻辑 —— // 自算版会在真实函数被改坏时继续绿(plan §3 第 4 条踩过三轮)。 // // 三类形状(依据 docs/channel-repair-plan.md §1.4 ④ 与 §3 第 1 条): // ① Result 信封 —— trae ExchangeToken:凭据在 `Result` 里,顶层没有 // ② SOLO 自定义 SSE —— trae 流式:`{"response":...}` 扁平帧,无 choices // ③ 嵌套 quota —— trae 余额:`pack.entitlement_base_info.quota.credits_limit` // // 任一形状被"改回方便测试的样子"(扁平/OpenAI 外壳/顶层字段),本判据立刻报红。 // TestFixtureShapeResultEnvelope ①:Result 信封必须被真实解析器认出来。 // // 真实上游(2026-10-17 实测,HTTP 200): // // {"ResponseMetadata":{...},"Result":{"Token":"eyJ...","RefreshToken":"..."}} // // 顶层**只有** ResponseMetadata + Result、没有 Token。若夹具写成扁平 // `{"Token":...}`,实现里「只读顶层」的错误会被完美掩盖 —— 这正是它藏了两轮的原因。 func TestFixtureShapeResultEnvelope(t *testing.T) { _, ps, _ := runtimePlugins(t) src, err := bundledAdapters.ReadFile("plugins/channels.lua") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", string(src)); err != nil { t.Fatal(err) } // 真实形状:凭据只在 Result 里 real := `{"ResponseMetadata":{"RequestId":"req-1","Action":"ExchangeToken"},` + `"Result":{"ClientID":"cid","Token":"ACCESS-FROM-RESULT",` + `"RefreshToken":"RT-ROTATED","TokenExpireAt":1800000000000,` + `"TokenExpireDuration":1209600000,"RefreshExpireAt":1807755929432}}` out, err := ps.Call("channels", "__trae_exchange_parse_probe", map[string]interface{}{ "body": real, }) if err != nil { t.Fatalf("探针调用失败: %v", err) } j := probeJSON(t, out) if j["error"] != nil { t.Fatalf("★ 真实 Result 信封解析失败: %v —— 检查是否只读顶层", j["error"]) } if got, _ := j["access"].(string); got != "ACCESS-FROM-RESULT" { t.Fatalf("★ access 必须来自 Result.Token,实得 %q", got) } if got, _ := j["refresh"].(string); got != "RT-ROTATED" { t.Fatalf("★ 轮换后的 refresh 必须来自 Result.RefreshToken,实得 %q", got) } // 反例:把这个形状的 Result 剥掉(= 旧的扁平夹具),解析器必须**报错**而不是 // 悄悄返回空 —— 否则"缺 Token"永远被误报成成功。 flat := `{"ResponseMetadata":{"RequestId":"req-1"},"Token":"SHOULD-NOT-RESOLVE"}` out2, err := ps.Call("channels", "__trae_exchange_parse_probe", map[string]interface{}{ "body": flat, }) if err != nil { t.Fatalf("探针调用失败: %v", err) } j2 := probeJSON(t, out2) // 注意:顶层 Token 是**兼容兜底**(上游可能改版),所以这里允许成功; // 但要钉住「Result 缺失时不会崩、也不会把 ResponseMetadata 当 Result」。 if j2["error"] == nil { if got, _ := j2["access"].(string); got != "SHOULD-NOT-RESOLVE" { t.Fatalf("顶层兜底路径应取顶层 Token,实得 %q", got) } } // 真正必须报错的形状:既无 Result.Token 也无顶层 Token bad := `{"ResponseMetadata":{"RequestId":"req-1"},"Result":{"ClientID":"cid"}}` out3, err := ps.Call("channels", "__trae_exchange_parse_probe", map[string]interface{}{ "body": bad, }) if err != nil { t.Fatalf("探针调用失败: %v", err) } j3 := probeJSON(t, out3) if j3["error"] == nil { t.Fatalf("★ 无 Token 的响应必须报错(否则「缺少 Token」会被误判成成功): %v", j3) } t.Logf("✓ Result 信封: access=%v refresh=%v;缺 Token 报错=%v", j["access"], j["refresh"], j3["error"]) } // TestFixtureShapeSoloSSE ②:trae 的 SOLO 自定义 SSE 必须被摊平成扁平 UnifiedChunk。 // // 真实上游帧(参考 trae.ts SOLO): // // event: output // data: {"response":"###","reasoning_content":null,"tool_calls":null} // // 内核只把 `data:` 行交给适配器(event 行在 provider.go 丢弃),且 // `json.Unmarshal(unified, &UnifiedChunk)` 解的是**扁平结构** —— // 返回 OpenAI 外壳 `{choices:[{delta}]}` 会被整体丢弃(91 帧全空)。 func TestFixtureShapeSoloSSE(t *testing.T) { vm, ps, _ := runtimePlugins(t) src, err := bundledAdapters.ReadFile("plugins/channels.lua") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", string(src)); err != nil { t.Fatal(err) } // 真实正文帧(注意:`reasoning_content` / `tool_calls` 显式 null,不是缺失) frames := []struct { name string frame string wantKey string wantText string }{ { name: "正文帧", frame: `{"response":"### 分析","reasoning_content":null,"tool_calls":null}`, wantKey: "content", wantText: "### 分析", }, { name: "推理帧", frame: `{"response":"","reasoning_content":"We need","tool_calls":null}`, wantKey: "reasoning_content", wantText: "We need", }, } for _, f := range frames { out, err := vm.Transform("channels-trae", "transform_stream_chunk", f.frame) if err != nil { t.Fatalf("%s: transform 失败: %v", f.name, err) } var ck map[string]interface{} if err := json.Unmarshal([]byte(out), &ck); err != nil { t.Fatalf("%s: 产出不是 JSON: %q", f.name, out) } // ★ 形状一:必须是**扁平**(顶层 content),不能是 OpenAI 外壳 if _, has := ck["choices"]; has { t.Fatalf("★ %s 不得返回 OpenAI 外壳(choices)—— 内核按 UnifiedChunk 解析,"+ "外壳字段会被全部丢弃: %s", f.name, out) } got, _ := ck[f.wantKey].(string) if got != f.wantText { t.Fatalf("★ %s 应摊平到顶层 %s=%q,实得 %q(原始产出 %s)", f.name, f.wantKey, f.wantText, got, out) } } t.Logf("✓ SOLO SSE 扁平形状正确(content / reasoning_content 均在顶层)") } // TestFixtureShapeNestedQuota ③:trae 余额在**两层嵌套**里,不在 pack 顶层。 // // 真实上游(2026-10-17 实测): // // pack[0] 免费 → entitlement_base_info.quota 无 credits_limit // pack[1] 每月登录赠送 → entitlement_base_info.quota.credits_limit = 500 // pack[2] 签到奖励 → entitlement_base_info.quota.credits_limit = 100 // usage_summary: {total_amount:600, consumed_amount:0} // // 旧实现读 `pack.credits_limit`(**根本不存在的字段**)⇒ 余额恒 0, // 而账号里其实有 600 —— 用户看到「额度显示异常」。 func TestFixtureShapeNestedQuota(t *testing.T) { _, ps, _ := runtimePlugins(t) src, err := bundledAdapters.ReadFile("plugins/channels.lua") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", string(src)); err != nil { t.Fatal(err) } // 真实形状:额度在 entitlement_base_info.quota 里(含一个无额度的免费包) real := `{"user_entitlement_pack_list":[` + `{"pack_type":"free","entitlement_base_info":{"quota":{"enable_free":true}}},` + `{"pack_type":"monthly","entitlement_base_info":{"quota":{"credits_limit":500}},` + ` "usage":{}},` + `{"pack_type":"checkin","entitlement_base_info":{"quota":{"credits_limit":100}},` + ` "usage":{"credits_amount":0}}` + `],"usage_summary":{"total_amount":600,"consumed_amount":0}}` out, err := ps.Call("channels", "__balance_total_probe", map[string]interface{}{ "platform": "trae", "body": real, }) if err != nil { t.Fatalf("探针调用失败: %v", err) } j := probeJSON(t, out) if j["error"] != nil { t.Fatalf("★ 嵌套 quota 解析失败: %v", j["error"]) } if got, _ := j["total"].(float64); got != 600 { t.Fatalf("★ 余额应为 500+100=600(嵌套 entitlement_base_info.quota.credits_limit),"+ "实得 %v —— 若为 0,检查是否又去读了 pack 顶层的 credits_limit(该字段不存在)", j["total"]) } // 反例:把额度放到**pack 顶层**(= 旧实现假设的形状)。真实上游不这么发, // 但实现保留了兼容路径 —— 这里钉住的是「嵌套路径是权威」,同时确认 // 顶层兼容不会把数字算错(不会重复累加)。 // // SHAPE-OK: 这段**刻意**用退化形状(额度挂 pack 顶层),因为它要测的正是 // 兼容兵底路径。生产形状由上面的嵌套夹具铉住;本文件里“生产/退化”两种 // 形状成对出现,才是“实现能区分两者”的证据。 // (若删掉这句,TestNoDegenerateFixturesInRepo 会报红 —— 这是故意的: // 它强迫每一个退化夹具显式声明自己是刻意的。) topLevel := `{"user_entitlement_pack_list":[{"credits_limit":50}]}` out2, err := ps.Call("channels", "__balance_total_probe", map[string]interface{}{ "platform": "trae", "body": topLevel, }) if err != nil { t.Fatalf("探针调用失败: %v", err) } j2 := probeJSON(t, out2) if j2["error"] != nil { t.Fatalf("顶层兼容路径不该报错: %v", j2["error"]) } if got, _ := j2["total"].(float64); got != 50 { t.Fatalf("顶层兼容路径应算 50,实得 %v", j2["total"]) } // 已用额度必须扣减(usage.credits_amount 也是嵌套的) consumed := `{"user_entitlement_pack_list":[` + `{"entitlement_base_info":{"quota":{"credits_limit":500}},` + ` "usage":{"credits_amount":120}}]}` out3, err := ps.Call("channels", "__balance_total_probe", map[string]interface{}{ "platform": "trae", "body": consumed, }) if err != nil { t.Fatalf("探针调用失败: %v", err) } j3 := probeJSON(t, out3) if got, _ := j3["total"].(float64); got != 380 { t.Fatalf("★ 余额应为 500-120=380(usage.credits_amount 是嵌套的),实得 %v", j3["total"]) } t.Logf("✓ 嵌套 quota: 600 / 扣减后 380 / 顶层兼容 50") } // probeJSON 取探针返回的 {json:{...}} 那层。 func probeJSON(t *testing.T, got interface{}) map[string]interface{} { t.Helper() m, ok := got.(map[string]interface{}) if !ok { t.Fatalf("探针返回值不是对象: %#v", got) } j, ok := m["json"].(map[string]interface{}) if !ok { t.Fatalf("探针返回值缺 json 层: %#v", m) } return j } // TestFixtureShapeGuardCoversAllThree 元判据:上三条**真的存在且真的咬到了 // 被测对象**。这是防「有人删掉形状判据」或「把判据改成不调真实解析器」的兜底。 // // 它读自己的源码文本(同目录下),断言: // ① 三个形状判据函数都在; // ② 它们**都调用了**插件探针(而不是自己另写一份解包逻辑 —— // 那样真实函数被改坏时判据会继续绿,这正是 trae 藏了两轮的原因)。 func TestFixtureShapeGuardCoversAllThree(t *testing.T) { self, err := os.ReadFile("channels_fixture_shapes_test.go") if err != nil { t.Fatalf("读不到自身源码(元判据靠它验证三条形状判据是否还在): %v", err) } s := string(self) // ① 三个形状判据必须在 for _, fn := range []string{ "TestFixtureShapeResultEnvelope", "TestFixtureShapeSoloSSE", "TestFixtureShapeNestedQuota", } { if !strings.Contains(s, "func "+fn+"(") { t.Fatalf("★ 形状判据 %s 不见了 —— 三类真实形状必须持续被钉住", fn) } } // ② 三条判据必须真的调用探针/真实适配器(不能退化成读源码字符串断言) // Result 信封 / 嵌套 quota → 探针(同一个真实解析器) // SOLO SSE → 已装载的 channels-trae 适配器 for _, anchor := range []string{ `__trae_exchange_parse_probe`, // ① Result 信封 `__balance_total_probe`, // ③ 嵌套 quota `vm.Transform("channels-trae", "transform_stream_chunk"`, // ② SOLO SSE } { if !strings.Contains(s, anchor) { t.Fatalf("★ 形状判据不再直接咬真实实现(缺 %s)—— "+ "自算版会在真实函数被改坏时继续绿", anchor) } } // ③ 探针必须真的在插件里导出(否则上面两条会在运行期变成"探针不存在") src, err := bundledAdapters.ReadFile("plugins/channels.lua") if err != nil { t.Fatal(err) } for _, probe := range []string{"__trae_exchange_parse_probe", "__balance_total_probe"} { if !strings.Contains(string(src), probe) { t.Fatalf("★ 插件缺探针 %s —— 形状判据将无从咬真实解析器", probe) } } t.Logf("✓ 三类形状判据齐全,且均直接咬真实实现") }