fix: 收口渠道源迁移与测试页合并

(cherry picked from commit 12bfd53f16)
This commit is contained in:
JianFeeeee
2026-10-19 17:21:22 +08:00
committed by root
parent 711ccdeea8
commit b0f33c188f
3 changed files with 209 additions and 124 deletions

View File

@ -0,0 +1,105 @@
package core
import (
"testing"
)
// ★ 升级后**旧版按账号拆的源必须被清掉**,否则同一个渠道的模型出现两遍。
//
// 真实事故(2026-10-19 用户报「同一个模型两个名字 / 源列表一堆不可读的东西」):
// 生产 config.yaml 里 6 对源并存 ——
//
// channels-trae-b8dd6b3df30e + channels-trae
// channels-buddy-buddy-main + channels-buddy
// …(trae/lobsterai/minimax/buddy/codearts/zcode 各一对)
//
// 新设计是「一通道一源」(`channels-<provider>`),换号靠同名 add_source 覆盖
// api_key。旧版是「一账号一源」(`channels-<provider>-<账号id>`)。升级时必须把
// 旧形态清掉,否则:
//
// ① 模型列表里同一模型出现多遍,只能靠 12 位 hex 区分(人读不出来)
// ② 旧源指向已不该用的账号,却可能因为注册更早而**先**被调度到
//
// 根因是形态假设错了:内核 `list_sources` 返回**裸数组**(见 kernel_actions.go
// 的 actionListSources,直接 `return out`),而迁移代码按 `{sources:[...]}`
// 信封去取 ⇒ 恒为 nil ⇒ 循环一次都不跑 ⇒ 静默什么都没清。
//
// ★ 本判据的关键是**用裸数组形态喂数据**(也就是内核真实返回的形状)。
//
// 如果按信封造夹具,即使代码是错的两边也"对上了",测不出任何东西 ——
// 这正是这个 bug 逃过全部既有判据的原因。
func TestChannelsLegacyPerAccountSourceIsCleaned(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.Fatalf("加载插件: %v", err)
}
if err := ps.SetPluginKernelPermission("channels",
[]string{"add_source", "remove_source", "list_sources", "status"}, false); err != nil {
t.Fatalf("授权: %v", err)
}
// 造存量:一个旧形态源(带账号 hex)+ 一个别的通道的旧源(不该被误删)。
mk := func(name string) {
if _, err := ps.Call("channels", "__add_source_for_test", map[string]interface{}{
"name": name,
"base_url": "https://example.invalid",
"adapter": "openai",
"api_key": "sk-x",
"models": []interface{}{"m1"},
}); err != nil {
t.Fatalf("造源 %s: %v", name, err)
}
}
mk("channels-trae-b8dd6b3df30e") // 旧形态:trae 的某个账号
mk("channels-trae-deadbeef0123") // 旧形态:trae 的另一个账号
mk("channels-buddy-buddy-main") // 别的通道 —— 不该被 trae 的同步碰到
mk("unrelated-source") // 与 channels 无关,绝不能被删
// 放一个**真实**的 trae 账号(走 account_add,不是探针):没有账号就无从
// 写源,也就走不到「写前先清旧源」那一步。account_add 本身也会同步一次源,
// 但那正是要测的路径。
if _, err := ps.Call("channels", "account_add", map[string]interface{}{
"platform": "trae", "account_id": "b8dd6b3df30e",
"credential": `{"access_token":"T","refresh_token":"R","nickname":"n"}`,
}); err != nil {
t.Fatalf("造账号: %v", err)
}
// ★ 只走 `syncAccountSource` → `syncProviderSource` 这条路(迁移清理就在这里)。
// 刻意**不用** reap_orphan_sources:它自己另有一套"形态 1 旧源"清理逻辑,
// 会把这里要测的迁移代码完全遮住 —— 用它做判据时,即使迁移代码是坏的
// (信封假设的 bug 仍在)判据也会绿,等于没测。
if _, err := ps.Call("channels", "__resync_sources_probe", map[string]interface{}{
"platform": "trae",
}); err != nil {
t.Fatalf("resync: %v", err)
}
names := map[string]bool{}
for _, s := range c.Sources() {
names[s.Name] = true
}
for _, legacy := range []string{"channels-trae-b8dd6b3df30e", "channels-trae-deadbeef0123"} {
if names[legacy] {
t.Errorf("旧形态源 %s 没被清掉 —— 同一渠道的模型会在列表里出现两遍,"+
"只能靠 12 位 hex 区分,用户读不出哪个是哪个", legacy)
}
}
if !names["channels-trae"] {
t.Error("新形态源 channels-trae 没建起来")
}
if names["unrelated-source"] != true {
t.Error("清理误伤了无关源 unrelated-source")
}
// ★ 这里才是真的越界判据:trae 的同步**只**该清 `channels-trae-` 前缀的旧源。
// 如果前缀匹配写成贪婪/过宽(例如 `channels-(.+)-`),buddy 的源会被误删。
if !names["channels-buddy-buddy-main"] {
t.Error("清理越界:trae 的同步删掉了 buddy 的源(前缀匹配过宽)")
}
}