package lua import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) // ============================================================================ // P3(通用版):把「夹具形状」本身变成判据,而不是逐个渠道写三条用例。 // ============================================================================ // // 为什么这条比「三个具体用例」重要(plan §3 第 1 条 / §1.4 ④): // // 本轮最大的教训**不是某个 bug**,而是「夹具形状错 ⇒ 判据必然虚假通过」。 // trae ExchangeToken 的「只读顶层、不认 Result 信封」在两个版本的扁平夹具 // `{"Token":...}` 下**全部判据都是绿的**,真实登录却报「缺少 Token」。 // // ⇒ 判据写得再严,只要喂进去的形状不是生产形状,它就毫无意义。 // 所以必须钉住的不是「行为」,而是「**喂进去的形状**」。 // // 本文件做两层(`channels_fixture_shapes_test.go` 的三条具体用例仍在,负责 // 行为断言;这里负责**形状本身**的守门): // // ① shapeFamilies 登记表:每族给出 // real —— 生产原文(引自实测抓包/参考实现注释) // naive —— 错误的「方便测试版」 // fact —— 对原始 JSON 的**结构事实**(real 必须满足,naive 必须不满足) // eval —— 用**真实实现**处理它 // 断言:fact(real) && !fact(naive) && eval(real) 符合该族契约。 // 注意**不比较 eval 的输出形状**:多族实现刻意保留了兼容兜底路径 // (例如 trae 余额同时认顶层与嵌套),拿输出比对会把「合法兼容」判成错误。 // 真正能区分真假的判据是 fact —— 它说的是「生产响应长什么样」。 // // ② 仓库扫描:对**特征明确**的退化形状做静态反查(只用于不会误报的那一族: // 把额度挪到 pack 顶层的 `{"credits_limit":`)。其余族的精确钉法是 ①, // 不靠文本匹配 —— 文本匹配对「Fatalf 文案里提到 choices」这类必然误报。 // shapeFamily 一类真实上游形状。 type shapeFamily struct { // name 族名(判据名里会带上它,便于定位)。 name string // why 为什么这族形状会被「方便的夹具」写坏(写进失败信息)。 why string // real 生产原文。 real string // naive 错误的「方便版」。必须**不满足** fact。 naive string // fact 结构事实:true 表示「这段 body 是生产形状」。 // 只用它做形状判定,不用它替代 eval。 fact func(raw map[string]interface{}) bool // eval 用真实实现处理一段 body。 eval func(body string) (map[string]interface{}, error) // assert 断言 eval(real) 的结果符合该族契约(行为层)。 assert func(t *testing.T, obs map[string]interface{}) } // shapeFamilies 是形状契约的**唯一登记处**。 // // 新增渠道若出现「信封 / 自定义 SSE / 嵌套字段」这类容易被夹具写坏的形状, // 必须在这里加一行 —— 元判据会检查三类基线族都在,且每行都真的调了被测实现。 func shapeFamilies(t *testing.T) []shapeFamily { t.Helper() return []shapeFamily{ { // ① Result 信封:凭据在 **Result 里**,顶层只有 ResponseMetadata。 name: "Result 信封", why: "顶层看起来「也」该有 Token,于是夹具顺手写成扁平 {\"Token\":...};" + "实现只读顶层时判据全绿 —— 真实缺陷就是这么藏了两轮", real: `{"ResponseMetadata":{"RequestId":"req-1","Action":"ExchangeToken"},` + `"Result":{"ClientID":"cid","Token":"ACCESS-FROM-RESULT",` + `"RefreshToken":"RT-ROTATED","TokenExpireAt":1800000000000}}`, naive: `{"ResponseMetadata":{"RequestId":"req-1"},` + `"Token":"ACCESS-FROM-RESULT"}`, fact: func(raw map[string]interface{}) bool { // 生产形状的判据:Result 是对象且其中有 Token,**顶层没有** Token。 r, ok := raw["Result"].(map[string]interface{}) if !ok { return false } if _, has := r["Token"]; !has { return false } _, topHas := raw["Token"] return !topHas }, eval: func(body string) (map[string]interface{}, error) { _, ps := shapesProbeVM(t) out, err := ps.Call("channels", "__trae_exchange_parse_probe", map[string]interface{}{"body": body}) if err != nil { return nil, err } return probeJSON(t, out), nil }, assert: func(t *testing.T, obs map[string]interface{}) { if obs["error"] != nil { t.Fatalf("★ 生产形状解析失败: %v —— 是否只读顶层、没认 Result 信封?", obs["error"]) } if got, _ := obs["access"].(string); got != "ACCESS-FROM-RESULT" { t.Fatalf("★ access 必须来自 Result.Token,实得 %q", got) } if got, _ := obs["refresh"].(string); got != "RT-ROTATED" { t.Fatalf("★ 轮换后的 refresh 必须来自 Result.RefreshToken,实得 %q", got) } }, }, { // ② SOLO 自定义 SSE:扁平帧,正文在 **response**,**没有 choices**。 name: "SOLO 自定义 SSE", why: "内核按 UnifiedChunk 解**扁平**结构;夹具若照 OpenAI 写成 " + "{choices:[{delta}]},适配器返回外壳也会「看起来对」,而真机是 91 帧全空", real: `{"response":"### 分析","reasoning_content":null,"tool_calls":null}`, naive: `{"choices":[{"delta":{"content":"### 分析"}}]}`, fact: func(raw map[string]interface{}) bool { // 生产形状:正文在顶层 response,且**没有** choices。 if _, has := raw["response"]; !has { return false } _, hasChoices := raw["choices"] return !hasChoices }, eval: func(body string) (map[string]interface{}, error) { vm, _ := shapesProbeVM(t) out, err := vm.Transform("channels-trae", "transform_stream_chunk", body) if err != nil { return nil, err } var ck map[string]interface{} if err := json.Unmarshal([]byte(out), &ck); err != nil { // 适配器对非 SOLO 帧返回 "" 是合法行为,不是夹具问题。 return map[string]interface{}{"raw": out}, nil } return ck, nil }, assert: func(t *testing.T, obs map[string]interface{}) { if _, has := obs["choices"]; has { t.Fatalf("★ 不得返回 OpenAI 外壳(choices)—— 内核按 UnifiedChunk 解析: %v", obs) } if got, _ := obs["content"].(string); got != "### 分析" { t.Fatalf("★ SOLO 正文必须摊平到顶层 content,实得 %q(%v)", got, obs) } }, }, { // ③ 嵌套 quota:额度在 **entitlement_base_info.quota.credits_limit**。 name: "嵌套 quota", why: "pack 顶层**没有** credits_limit(该字段根本不存在);" + "夹具若照想象的形状写成 pack.credits_limit,实现读错字段也全绿。" + "注意实现**刻意**保留了顶层兼容路径,所以「输出形状」不能当判据 ——" + "能区分真假的是「生产响应里额度在哪一层」", 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}}`, // 退化版:额度直接挂 pack 顶层(= 旧实现假设的形状,真实上游不发)。 naive: `{"user_entitlement_pack_list":[{"credits_limit":500},{"credits_limit":100}]}`, fact: func(raw map[string]interface{}) bool { list, ok := raw["user_entitlement_pack_list"].([]interface{}) if !ok || len(list) == 0 { return false } nested := 0 for _, it := range list { e, ok := it.(map[string]interface{}) if !ok { return false } if _, top := e["credits_limit"]; top { return false // 顶层有 credits_limit ⇒ 不是生产形状 } base, ok := e["entitlement_base_info"].(map[string]interface{}) if !ok { continue } q, ok := base["quota"].(map[string]interface{}) if !ok { continue } if _, ok := q["credits_limit"]; ok { nested++ } } return nested > 0 }, eval: func(body string) (map[string]interface{}, error) { _, ps := shapesProbeVM(t) out, err := ps.Call("channels", "__balance_total_probe", map[string]interface{}{"platform": "trae", "body": body}) if err != nil { return nil, err } return probeJSON(t, out), nil }, assert: func(t *testing.T, obs map[string]interface{}) { if obs["error"] != nil { t.Fatalf("★ 生产形状解析失败: %v", obs["error"]) } if got, _ := obs["total"].(float64); got != 600 { t.Fatalf("★ 余额应为 500+100=600(嵌套 entitlement_base_info.quota.credits_limit),"+ "实得 %v —— 若为 0,检查是否又去读 pack 顶层的 credits_limit(该字段不存在)", got) } }, }, } } // TestRealUpstreamShapesArePinned 是 P3 的**通用**判据。 // // 每族三重断言: // 1. fact(real) —— 生产原文**确实是**该族形状(防止有人把「生产原文」写错) // 2. !fact(naive) —— 退化形状**确实不是**该族形状(证明两者可区分) // 3. assert(eval(real)) —— 真实实现能吃下生产形状 // // 第 1、2 条是关键:它们证明「判据在解结构」,而不是「碰巧对任何输入都成功」。 func TestRealUpstreamShapesArePinned(t *testing.T) { fams := shapeFamilies(t) if len(fams) < 3 { t.Fatalf("★ 形状契约至少要覆盖 3 族(Result 信封 / SOLO SSE / 嵌套 quota),实得 %d", len(fams)) } for _, f := range fams { t.Run(f.name, func(t *testing.T) { if f.real == "" || f.naive == "" || f.fact == nil || f.eval == nil || f.assert == nil { t.Fatalf("族 %q 登记不完整(real/naive/fact/eval/assert 全必填)—— "+ "只钉生产形状无法证明判据真的在解结构", f.name) } // ① 生产原文必须真的满足该族的结构事实 realRaw := mustJSONObject(t, f.real, f.name, "real") if !f.fact(realRaw) { t.Fatalf("★ 族 %q:登记的「生产原文」不满足结构事实 —— "+ "要么原文抄错了,要么 fact 写错了(两者都会让判据失去意义):\n %s", f.name, f.real) } // ② 退化形状必须不满足(证明 fact 能区分,不是恒真) naiveRaw := mustJSONObject(t, f.naive, f.name, "naive") if f.fact(naiveRaw) { t.Fatalf("★ 族 %q:退化形状也满足结构事实 ⇒ fact 是恒真式,"+ "它区分不了真假夹具(这正是「夹具形状错⇒判据虚假通过」的机制):\n %s", f.name, f.naive) } // ③ 真实实现必须能吃下生产形状,且行为符合契约 obs, err := f.eval(f.real) if err != nil { t.Fatalf("%s: 生产形状求值失败: %v", f.name, err) } f.assert(t, obs) // ④ 退化形状**不得**被当成同一族的成功解析:要么报错,要么明确 // 走了兼容路径(此时 fact 已经证明它不是生产形状)。 // 这里只记录,不做断言 —— 兼容路径是刻意的(见 why)。 naiveObs, naiveErr := f.eval(f.naive) t.Logf("✓ %s: real→%v | naive→(err=%v, obs=%v)", f.name, obs, naiveErr, naiveObs) }) } } func mustJSONObject(t *testing.T, s, fam, which string) map[string]interface{} { t.Helper() var v map[string]interface{} if err := json.Unmarshal([]byte(s), &v); err != nil { t.Fatalf("族 %q 的 %s 形状不是合法 JSON 对象: %v\n %s", fam, which, err, s) } return v } // TestShapeFamilyTableIsHonest 元判据:登记表本身不许糊弄。 // // ① 三类基线族必须在(Result 信封 / SOLO SSE / 嵌套 quota); // ② 每族的 eval 必须真的打到被测实现 —— 给一段垃圾 body 时它必须有反应 // (报错,或产出与生产形状不同的观测)。恒等函数会在这里被抓住。 func TestShapeFamilyTableIsHonest(t *testing.T) { fams := shapeFamilies(t) want := []string{"Result 信封", "SOLO 自定义 SSE", "嵌套 quota"} got := map[string]bool{} for _, f := range fams { got[f.name] = true } for _, w := range want { if !got[w] { t.Fatalf("★ 形状表缺基线族 %q —— 三类真实形状必须持续被钉住(plan §1.4 ④)", w) } } for _, f := range fams { realObs, err := f.eval(f.real) if err != nil { t.Fatalf("%s: 生产形状求值失败: %v", f.name, err) } junkObs, junkErr := f.eval(`<<>>`) if junkErr == nil && samePrimitives(realObs, junkObs) { t.Fatalf("★ 族 %q 的 eval 对垃圾输入与生产输入返回相同观测 ⇒ "+ "它不是真的在调被测实现(恒等判据毫无意义)", f.name) } } t.Logf("✓ 形状表 %d 族,基线齐全且 eval 真的打到被测实现", len(fams)) } // samePrimitives 只比较"标量键值都相同",用于检测恒等实现。 func samePrimitives(a, b map[string]interface{}) bool { if len(a) != len(b) { return false } for k, av := range a { bv, ok := b[k] if !ok { return false } switch x := av.(type) { case string: if y, ok := bv.(string); !ok || x != y { return false } case float64: if y, ok := bv.(float64); !ok || x != y { return false } case bool: if y, ok := bv.(bool); !ok || x != y { return false } case nil: if bv != nil { return false } default: return false } } return true } // --------------------------------------------------------------------------- // ② 仓库扫描:只对**特征明确**的退化形状做静态反查 // --------------------------------------------------------------------------- // // 为什么只留一条规则:文本匹配对另外两族**必然误报** —— // · SOLO:判据自己会写 `t.Fatalf("不得返回 choices …")`,天然含 choices; // · Result:真夹具里 `"Result":{…,"Token":…}` 与顶层 `"Token":` 是同一行。 // 那两族的精确钉法是上面的 fact 判据(对原始 JSON 结构判定),不是文本。 // // 剩下这一条规则的特征是**不会误报**的:把额度写到 pack 顶层必然写成 // `{"credits_limit":`(花括号紧跟键名),而生产形状一定写成 // `"quota":{"credits_limit":`(键名前是 `"quota":`)。 // degenerateFixturePattern 一条"退化夹具"特征。 type degenerateFixturePattern struct { name string trigger string // 文件里出现它 ⇒ 该文件在测这个渠道 forbidden string // 出现它 ⇒ 疑似把生产形状改成了方便版 why string } var degenerateFixturePatterns = []degenerateFixturePattern{ { name: "trae 余额(嵌套 quota)", trigger: "user_entitlement_pack_list", forbidden: `{"credits_limit":`, why: "生产把额度放在 entitlement_base_info.quota.credits_limit(pack 顶层" + "**没有**该字段,实测上游从不发);`{\"credits_limit\":` 是把额度挪到顶层的" + "退化形状,会让「读错层级」这个缺陷继续绿(余额恒 0 就是这么来的)", }, } // TestNoDegenerateFixturesInRepo 扫 internal/**/*_test.go,任何"可疑退化夹具" // 都必须带 `// SHAPE-OK: <理由>` 显式豁免。 // // 为什么扫描而不是只钉三条用例:三条用例只能钉住**它们自己**用的夹具; // 下一次有人在**别的**测试里写一个退化夹具,那三条照样绿。这条扫描对着 // 整个仓库,是「防再犯」的那一半。 func TestNoDegenerateFixturesInRepo(t *testing.T) { root, err := repoInternalDir() if err != nil { t.Fatalf("定位 internal/ 失败: %v", err) } var files []string err = filepath.Walk(root, func(p string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } if strings.HasSuffix(p, "_test.go") { files = append(files, p) } return nil }) if err != nil { t.Fatalf("遍历 internal/: %v", err) } if len(files) == 0 { t.Fatal("没扫到任何 _test.go —— 扫描失效(这本身比告警更糟)") } var hits []string for _, f := range files { b, err := os.ReadFile(f) if err != nil { continue } rel, _ := filepath.Rel(root, f) // 本文件自身在描述规则(含特征字面量),跳过。 if strings.HasSuffix(f, "channels_fixture_shapes_general_test.go") { continue } code := stripGoComments(string(b)) for _, pat := range degenerateFixturePatterns { if !strings.Contains(code, pat.trigger) { continue } if !strings.Contains(code, pat.forbidden) { continue } if strings.Contains(string(b), "SHAPE-OK:") { continue // 显式豁免,理由写在文件里 } hits = append(hits, rel+" ["+pat.name+"] 含 "+pat.forbidden) } } if len(hits) > 0 { t.Fatalf("★ 疑似退化夹具(形状与生产不一致 ⇒ 判据必然虚假通过):\n %s\n\n"+ "若这**确实**是在测兼容/兜底路径,请在文件里加一行\n"+ " // SHAPE-OK: <为什么这个退化形状是刻意的>\n"+ "历史代价:trae 的 Result 信封缺陷就是这样藏了两轮(plan §1.4 ④)。\n"+ "规则理由:\n%s", strings.Join(hits, "\n "), whyList()) } t.Logf("✓ 扫描 %d 个 _test.go,无未豁免的退化夹具(%d 条特征规则)", len(files), len(degenerateFixturePatterns)) } func whyList() string { var b strings.Builder for _, p := range degenerateFixturePatterns { b.WriteString(" · " + p.name + ": " + p.why + "\n") } return b.String() } // stripGoComments 去掉 `//` 行注释与 `/* */` 块注释。 // // 只做够用的一层:不解析字符串字面量里的 `//`(例如 URL "https://…")。 // 略微保守(可能少剥一点)比误剥安全 —— 误剥会让扫描漏报,少剥只会多报, // 而多报可以由 SHAPE-OK 显式豁免。 func stripGoComments(s string) string { var b strings.Builder b.Grow(len(s)) lines := strings.Split(s, "\n") inBlock := false for _, line := range lines { if inBlock { if i := strings.Index(line, "*/"); i >= 0 { inBlock = false line = line[i+2:] } else { b.WriteString("\n") continue } } trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "//") { b.WriteString("\n") continue } if i := strings.Index(line, "/*"); i >= 0 && !strings.Contains(line[:i], `"`) { if j := strings.Index(line[i:], "*/"); j >= 0 { line = line[:i] + line[i+j+2:] } else { line = line[:i] inBlock = true } } b.WriteString(line) b.WriteString("\n") } return b.String() } // repoInternalDir 从测试工作目录(internal/lua)向上找 internal/。 func repoInternalDir() (string, error) { wd, err := os.Getwd() if err != nil { return "", err } for d := wd; d != "/" && d != ""; d = filepath.Dir(d) { if filepath.Base(d) == "internal" { return d, nil } } return filepath.Dir(wd), nil } // shapesProbeVM 提供形状判据共用的插件 VM(插件自装适配器需要它)。 // // 用 zcodeHarness 而不是 runtimePlugins:后者给 adapters/plugins 各一个独立 // 临时目录,而 installZcodeAdapter 要求两者是兄弟目录(见 zcodeHarness 注释)。 func shapesProbeVM(t *testing.T) (*VM, *Plugins) { t.Helper() return zcodeHarness(t) }