package core import ( "strings" "testing" "llmsproxy/internal/lua" ) // channels_pool_test:对齐参考 account.* / model.* / provider.* 契约的行为测试。 // 账号池增删改、上游源随 enabled/模型开关同步、供应商整体开关、备份导出。 func poolCore(t *testing.T) (*Core, *lua.Plugins) { t.Helper() 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) } _ = ps.SetPluginKernelPermission("channels", []string{"add_source", "list_sources", "remove_source", "status", "probe_all", "reset_health"}, false) return c, ps } // listSourcesViaKernel 直查内核 list_sources(actionListSources 返回 []map)。 func listSourcesViaKernel(t *testing.T, c *Core) []map[string]interface{} { t.Helper() out, err := c.Invoke(nil, "list_sources", map[string]interface{}{}) if err != nil { t.Fatalf("list_sources: %v", err) } if ms, ok := out.([]map[string]interface{}); ok { return ms } raw, _ := out.([]interface{}) var srcs []map[string]interface{} for _, s := range raw { if m, ok := s.(map[string]interface{}); ok { srcs = append(srcs, m) } } return srcs } func invoke(t *testing.T, ps *lua.Plugins, fn, body string) map[string]interface{} { t.Helper() out, err := ps.InvokeHandler("channels", fn, &lua.HandlerRequest{Method: "POST", Body: body}) if err != nil { t.Fatalf("invoke %s: %v", fn, err) } return out.JSON.(map[string]interface{}) } // asArr 规整桥接后的数组。Lua 空表过桥是 map[string]interface{}(无法与空数组 // 区分,见 vm.go luaTableToGoValue);内核 list_sources 的 models 过桥是 []string。 func asArr(v interface{}) []interface{} { switch a := v.(type) { case []interface{}: return a case []string: out := make([]interface{}, len(a)) for i, s := range a { out[i] = s } return out } return nil } func srcHasAccount(srcs []map[string]interface{}, needle string) (map[string]interface{}, bool) { for _, sm := range srcs { if name, _ := sm["name"].(string); strings.Contains(name, needle) { return sm, true } } return nil, false } // TestChannelsAccountPoolBehavior:账号池 CRUD + 上游源同步 + 模型/供应商开关 + 备份。 func TestChannelsAccountPoolBehavior(t *testing.T) { c, ps := poolCore(t) t.Run("账号增删改", func(t *testing.T) { m := invoke(t, ps, "account_add", `{"platform":"minimax","nickname":"我的号"}`) if m["ok"] != true { t.Fatalf("account_add: %v", m) } aid, _ := m["account_id"].(string) if aid == "" { t.Fatalf("account_add 应返回 account_id: %v", m) } m = invoke(t, ps, "account_list", `{"platform":"minimax"}`) accts := asArr(m["accounts"]) if len(accts) != 1 { t.Fatalf("应有1个账号: %d", len(accts)) } a := accts[0].(map[string]interface{}) if a["nickname"] != "我的号" || a["enabled"] != true { t.Fatalf("账号字段: %v", a) } invoke(t, ps, "account_update", `{"platform":"minimax","account_id":"`+aid+`","patch":{"nickname":"改名"}}`) m = invoke(t, ps, "account_list", `{"platform":"minimax"}`) if asArr(m["accounts"])[0].(map[string]interface{})["nickname"] != "改名" { t.Fatalf("改名失败") } invoke(t, ps, "account_delete", `{"platform":"minimax","account_id":"`+aid+`"}`) m = invoke(t, ps, "account_list", `{"platform":"minimax"}`) if n := len(asArr(m["accounts"])); n != 0 { t.Fatalf("删除后应为0, 实得 %d", n) } }) t.Run("模型开关同步上游源", func(t *testing.T) { m := invoke(t, ps, "account_add", `{"platform":"minimax","credential":"MM-KEY"}`) aid, _ := m["account_id"].(string) sm, ok := srcHasAccount(listSourcesViaKernel(t, c), aid) if !ok { t.Fatal("带凭据账号应已同步源") } if n := len(asArr(sm["models"])); n != 3 { t.Fatalf("源应有3模型, 实得 %d", n) } invoke(t, ps, "model_set_disabled", `{"platform":"minimax","model_id":"MiniMax-M3","disabled":true}`) sm, _ = srcHasAccount(listSourcesViaKernel(t, c), aid) if n := len(asArr(sm["models"])); n != 2 { t.Fatalf("关1模型后源应2模型, 实得 %d", n) } invoke(t, ps, "model_set_all_disabled", `{"platform":"minimax","disabled":true}`) m = invoke(t, ps, "provider_status", `{}`) for _, s := range asArr(m["statuses"]) { x := s.(map[string]interface{}) if x["id"] == "minimax" && x["closed"] != true { t.Fatalf("全关模型后应 closed=true: %v", x) } } invoke(t, ps, "model_set_all_disabled", `{"platform":"minimax","disabled":false}`) }) t.Run("enabled开关同步上游源", func(t *testing.T) { m := invoke(t, ps, "account_list", `{"platform":"minimax"}`) accts := asArr(m["accounts"]) if len(accts) == 0 { t.Fatal("上个用例应留有账号") } aid := accts[0].(map[string]interface{})["id"].(string) before := len(listSourcesViaKernel(t, c)) invoke(t, ps, "account_update", `{"platform":"minimax","account_id":"`+aid+`","patch":{"enabled":false}}`) if n := len(listSourcesViaKernel(t, c)); n != before-1 { t.Fatalf("停用账号应删源: 前 %d 后 %d", before, n) } invoke(t, ps, "account_update", `{"platform":"minimax","account_id":"`+aid+`","patch":{"enabled":true}}`) if n := len(listSourcesViaKernel(t, c)); n != before { t.Fatalf("启用账号应重建源: 前 %d 后 %d", before, n) } }) t.Run("供应商整体开关", func(t *testing.T) { invoke(t, ps, "provider_set_enabled", `{"platform":"minimax","enabled":false}`) if _, ok := srcHasAccount(listSourcesViaKernel(t, c), "minimax"); ok { t.Fatal("关闭渠道后不应有 minimax 源") } m := invoke(t, ps, "provider_status", `{}`) for _, s := range asArr(m["statuses"]) { x := s.(map[string]interface{}) if x["id"] == "minimax" && x["closed"] != true { t.Fatalf("关闭后应 closed: %v", x) } } invoke(t, ps, "provider_set_enabled", `{"platform":"minimax","enabled":true}`) if _, ok := srcHasAccount(listSourcesViaKernel(t, c), "minimax"); !ok { t.Fatal("开启渠道后应有 minimax 源") } }) t.Run("备份导出含凭据", func(t *testing.T) { invoke(t, ps, "account_add", `{"platform":"cline","credential":"CL-KEY","nickname":"cl1"}`) exp := invoke(t, ps, "backup_export", `{}`) payload, _ := exp["payload"].(map[string]interface{}) if payload == nil { t.Fatal("backup_export 应返回 payload") } creds, _ := payload["credentials"].(map[string]interface{}) found := false for _, v := range creds { if v == "CL-KEY" { found = true } } if !found { t.Fatalf("备份应含凭据明文: %v", creds) } }) }