mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
P1-1 anthropic 基座补 build_headers(x-api-key + anthropic-version) 根因:基座只有静态 anthropic-version、没有 build_headers ⇒ 内核兜底发 Authorization: Bearer,而 Anthropic 协议要 x-api-key。 ★ 证据冲突(计划 P1-1 原文一半有误):zcode 的 /zcode-plan/anthropic/v1/messages 虽然**说 Anthropic 协议**,鉴权却是 Bearer <zcode_jwt>(参考 zcode-transport.ts:121-125;全仓 grep x-api-key = 0 次命中)。故 zcode 必须**显式覆盖** build_headers 回到 Bearer, 否则基座的正向修复会把它从"对"改成"错"。 内核改动:internal/provider/provider.go —— 兜底 Bearer 让位给 x-api-key, 避免同一请求带两套鉴权(部分 Anthropic 兼容上游对歧义鉴权直接 401)。 P1-2 channels-cline 的 build_headers 补 workos: 前缀 根因:参考 cline.ts 发 `Bearer workos:<jwt>`,而池里存的是第二步 register 换回的裸 token ⇒ 之前无 build_headers,内核兜底发裸 Bearer。 幂等:已带前缀(含大小写变体)不重复拼。 P1-3 channels-gemini 只确认不改 参考 gemini.ts:353-366 同样是 OAuth Bearer + 身份五头、刻意不发 x-goog-api-key ⇒ 我们现状本来就对。加判据把这个结论钉住,防下轮照 P1-1 的模子把它改坏。 P2 源名契约:仅 [A-Za-z0-9_-],禁 : 与 / 根因:: 与 / 是限定名分隔符,出现在源名里会让 `源名:模型名` 有两种都 自圆其说的切法,同一请求串在不同部署下可能路由到不同源,且**不报错**。 校验加在写入口 core.normalizeSource(硬拒,API 400);启动时只告警, 避免存量带 : 的源名让升级 brick 掉整个网关。 解析入口 splitPinned 不做校验(解析不该拒绝已有数据)。 P3 夹具形状判据(本轮最重要的一条) 根因:trae 的 Result 信封缺陷在扁平夹具下所有判据全绿,真实登录却报 「缺少 Token」—— 夹具形状错 ⇒ 判据必然虚假通过。 通用形状层:shapeFamilies 登记表,每族给 real/naive/fact/eval/assert, 断言 fact(real) 且 !fact(naive) 且 assert(eval(real))。 刻意**不比较 eval 输出形状**:trae 余额实现合法保留顶层兼容路径, 拿输出比对会把合法兼容判成错误;能区分真假的是 fact(额度在哪一层)。 另有仓库扫描:对特征明确的退化形状静态反查,需退化夹具时写 SHAPE-OK 豁免。 判据:11 个文件 / 30 用例。全部变异已验证会报红(9 处)。
507 lines
19 KiB
Go
507 lines
19 KiB
Go
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(`<<<not json at all>>>`)
|
||
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)
|
||
}
|