package core import ( "testing" ) // 「已失效模型」判据必须严格 —— 误判的代价是把**可用模型藏 30 天**。 // // 参考为这些判据写了很长的注释(都是实测误判教训),本判据把它们钉住: // // - `model_not_found` 在遥测/日志里常作**字段名**出现(后面还有内容), // 只在它"独占一句话成分"时才算阳性; // - rate limit / quota / unauthor / forbidden 一律**不是**下架; // - `expired` 必须带主语(token/session/key/…),否则会误杀正常下架文案 // `model expired-v3 does not exist`; // - 只认 found/exist,**不认** available/supported(那是"换套餐就能恢复")。 func TestDeadModelGoneJudgement(t *testing.T) { 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) } cases := []struct { msg string want bool why string }{ // ── 阳性:真的是下架 ── {"model not found", true, "经典文案"}, {"unknown model: foo", true, "unknown model"}, {"no such model", true, "no such model"}, {"The model does not exist", true, "does not exist"}, {"error: model_not_found", true, "独占一句话(行尾)"}, {"model_not_found.", true, "后面是句末标点"}, // ── 阴性:看着像但**不是**下架 ── {"error_class=model_not_found, retry allowed", false, "字段名后面还有内容(初版误判下架,实测教训)"}, {"the model_not_found metric was not exported", false, "作为指标名出现(实测教训)"}, {"rate limit exceeded for model foo", false, "限流换时段就能用"}, {"quota exceeded", false, "额度问题"}, {"insufficient balance", false, "余额问题"}, {"401 unauthorized", false, "认证问题"}, {"invalid api key", false, "认证问题"}, {"token expired", false, "token 过期(带主语)"}, // ⚠️ 这条最微妙:expired 无主语时**不**能触发 NOT_GONE, // 否则会误杀 "model expired-v3 does not exist" 这类真实下架文案。 {"model expired-v3 does not exist", true, "expired 无主语 → 不算认证失败;does not exist 才是真判据"}, // ⚠️ 两条更正(对照参考实现的实际行为,不是注释字面): // 1. `lease expired` 单独出现**不算**下架 —— 它是租约/认证问题。 // 2. 即便带上 does not exist 也**仍不算**:参考的判定顺序是 // NOT_GONE 先于 GONE(`isModelGoneError` 第 265 行先查排除表), // 一旦命中排除词就直接 false、不再看后面的不存在语义。 // 我最初按注释字面以为"带主语才对、带上就想判下架",是读错了。 {"the model lease expired", false, "租约/认证问题,不是下架"}, {"the model lease expired and does not exist", false, "参考的顺序是 NOT_GONE 先行 ⇒ 命中排除词即 false"}, // available/supported 不算(换套餐就能恢复) {"model not available in your region", false, "available 不是 found/exist —— 换地域就能用(参考明确点名)"}, {"", false, "空串"}, } for _, tc := range cases { out, err := ps.Call("channels", "__is_model_gone_probe", map[string]interface{}{"message": tc.msg}) if err != nil { t.Fatalf("探针: %v", err) } w, _ := out.(map[string]interface{}) j, _ := w["json"].(map[string]interface{}) got, _ := j["gone"].(bool) if got != tc.want { t.Errorf("isModelGone(%q) = %v,应为 %v(%s)", tc.msg, got, tc.want, tc.why) } } } // 记录-剔除-恢复 的闭环,以及**不拿远端目录反推**这条纪律。 func TestDeadModelRecordAndPrune(t *testing.T) { 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) } before := modelIDsFor(t, ps, "zcode") if len(before) == 0 { t.Fatal("zcode 应有兜底模型") } victim := before[0] // 记录一条阳性证据 → 该模型必须从列表消失 if _, err := ps.Call("channels", "__record_dead_probe", map[string]interface{}{"platform": "zcode", "model_id": victim, "reason": "model not found"}); err != nil { t.Fatalf("记录探针: %v", err) } after := modelIDsFor(t, ps, "zcode") for _, id := range after { if id == victim { t.Errorf("已失效模型 %q 仍出现在列表里(应剔除)", victim) } } if len(after) != len(before)-1 { t.Errorf("剔除后应少一个:前 %d 后 %d", len(before), len(after)) } // model_clear_dead 应把它恢复回来(用户确认上游又上了 / 误判时用) m, err := ps.Call("channels", "model_clear_dead", map[string]interface{}{"platform": "zcode", "model_id": victim}) if err != nil { t.Fatalf("model_clear_dead: %v", err) } w, _ := m.(map[string]interface{}) j, _ := w["json"].(map[string]interface{}) if j == nil || j["ok"] != true { t.Fatalf("model_clear_dead 失败: %#v", m) } restored := modelIDsFor(t, ps, "zcode") found := false for _, id := range restored { if id == victim { found = true } } if !found { t.Errorf("clear 后 %q 应恢复,实得 %v", victim, restored) } } // 目录闸门:失败后进入冷却,冷却期内不重复拉(照参考 remote-catalog-gate)。 // 判据用冷却状态本身,不依赖真实网络。 func TestRemoteCatalogGateCooldown(t *testing.T) { 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) } // 初始不冷却 g := func() (bool, bool) { out, err := ps.Call("channels", "__catalog_gate_probe", map[string]interface{}{"platform": "zcode"}) if err != nil { t.Fatalf("探针: %v", err) } w, _ := out.(map[string]interface{}) j, _ := w["json"].(map[string]interface{}) began, _ := j["began"].(bool) cooling, _ := j["cooling"].(bool) return began, cooling } if _, cooling := g(); cooling { t.Error("初始不该处于冷却") } // 模拟一次失败 → 进冷却 if _, err := ps.Call("channels", "__catalog_gate_fail_probe", map[string]interface{}{"platform": "zcode"}); err != nil { t.Fatalf("失败探针: %v", err) } began, cooling := g() if !cooling { t.Error("失败后必须进入冷却(否则会对坏上游反复打 —— 参考点名的首屏卡死)") } if began { t.Error("冷却期内不该再发起拉取") } } // modelIDsFor 读某平台当前对外的模型 id 列表。 func modelIDsFor(t *testing.T, ps interface { Call(string, string, map[string]interface{}) (interface{}, error) }, plat string) []string { t.Helper() m, err := ps.Call("channels", "model_list", map[string]interface{}{"platform": plat}) if err != nil { t.Fatalf("model_list(%s): %v", plat, err) } w, _ := m.(map[string]interface{}) j, _ := w["json"].(map[string]interface{}) var out []string if arr, ok := j["models"].([]interface{}); ok { for _, it := range arr { if mm, ok := it.(map[string]interface{}); ok { if id, ok := mm["id"].(string); ok { out = append(out, id) } } } } return out }