Files
ModelRouter/internal/core/channels_loomy_test.go
JianFeeeee 9bec158d2e chore(main): 回流 release/v2.0.x(v2.0.12)—— 内容对齐,main 无独有内容
按发布纪律回流。**注意这是内容对齐而非 merge**,理由与依据:

1. 纪律禁止 merge 回主干,因为 merge 会把发布分支的**版本号变更**带进 main。
   本项目版本号**只由 `git describe --tags` 推导**(Makefile VERSION),
   仓库里没有任何文件存放版本号 ⇒ 回流不存在版本污染风险。
2. 逐项核对确认 **main 是 release 的内容严格子集**:
   - `main` 独有的**文件**:0 个;
   - `internal/lua/plugins.go` / `internal/core/kernel_actions.go` 等内核文件
     main 独有行数:0;
   - `deploy.sh` 那 14 行只是 emoji 清理(release 刻意去掉了 emoji);
   - `channels.lua` 那 333 行是**旧版内容**(旧 exports 表、旧模型名单)。
   - main 独有的 9 个提交按 patch-id **全部已在 release 中等效存在**
     (`git cherry` 逐条列出均为 `-`,即已合入)。
3. 故回流 = 把 release 的内容带到 main,等价于"只捡拾修复、不带版本号",
   且不会丢任何 main 独有的修复。

本次带回 v2.0.12 的全部内容(37 个提交):
- 源串行 + 按模型节流(照 model-gate)
- 已失效模型存储 + 远端目录闸门
- 余额分档选号
- 模型能力元数据(models.dev,isFree 权威判定)
- **凭据续期修复**(此前恒报 ok:true 却一个请求都不发)
- 通用件 TTL 缓存 + 串行锁;account_test/account_adopt/account_delete 修正
- 内核级:出站授权持久化重启后不生效、`__llmsproxy_plugin_dir` ground truth

校验:`git diff release/v2.0.x` = 0 行;`go build -tags luajit ./...` 通过。
2026-10-14 16:52:58 +08:00

178 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
)
// loomyHarness:Loomy 微信扫码长轮询的测试台。
//
// 参考语义(loomy-wechat.ts 点名,早期版本**读反了**导致"扫码后卡在已扫码"):
//
// 408 = 等待扫码;404 = 已扫码待确认(继续轮询);
// 405 = **已确认**(wx_code 就在这一帧);403 = 取消;402 = 失效
type loomyHarness struct {
t *testing.T
ps *lua.Plugins
ts *httptest.Server
polls int32
confirmAt int32
gotAuth string
}
func newLoomyHarness(t *testing.T) *loomyHarness {
t.Helper()
h := &loomyHarness{t: t, confirmAt: 3}
h.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/connect/qrconnect"):
w.Header().Set("Content-Type", "text/html")
// 夹具必须用**真实微信页面的形态**:uuid 内嵌在
// <img class="js_qrcode_img" src="/connect/qrcode/<uuid>"/>。
// 用别的形态(如 var uuid = "...")测出来的正则不是生产要用的那条。
fmt.Fprint(w, `<html><img class="js_qrcode_img" src="/connect/qrcode/UUID-TEST-1"/></html>`)
case strings.HasPrefix(r.URL.Path, "/connect/l/qrconnect"):
n := atomic.AddInt32(&h.polls, 1)
w.Header().Set("Content-Type", "text/plain")
if n == 1 {
fmt.Fprint(w, "window.wx_errcode=408;") // 等待扫码
return
}
if n < atomic.LoadInt32(&h.confirmAt) {
fmt.Fprint(w, "window.wx_errcode=404;") // 已扫码待确认
return
}
fmt.Fprint(w, `window.wx_errcode=405;window.wx_code='WX-CODE-1';`) // 已确认
case r.URL.Path == "/login/thirdAccount/bind/auth":
h.gotAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"code":0,"data":{"rcode":"RCODE-1","bind":1,"nickname":"loomy-dev","userid":"u5"}}`)
default:
w.WriteHeader(404)
}
}))
t.Cleanup(h.ts.Close)
c := kernelTestCore(t)
h.ps = c.Plugins()
src, err := readBundledChannels(t)
if err != nil {
t.Fatal(err)
}
if err := h.ps.LoadSource("channels", src); err != nil {
t.Fatal(err)
}
_ = h.ps.SetPluginKernelPermission("channels", []string{"add_source", "list_sources", "status"}, false)
if _, err := h.ps.Call("channels", "set_endpoints", map[string]interface{}{
"platform": "loomy",
"wechat": map[string]interface{}{
"auth_url": h.ts.URL + "/connect/qrconnect?state={state}",
"qr_url": h.ts.URL + "/connect/qrcode/{uuid}",
"poll_url": h.ts.URL + "/connect/l/qrconnect",
"account_base": h.ts.URL,
},
}); err != nil {
t.Fatalf("set_endpoints: %v", err)
}
return h
}
func (h *loomyHarness) begin() (map[string]interface{}, string) {
out, err := h.ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{
Method: "POST", Body: `{"platform":"loomy"}`, Role: "admin",
})
if err != nil {
h.t.Fatal(err)
}
j := out.JSON.(map[string]interface{})
return j, j["session"].(map[string]interface{})["id"].(string)
}
func (h *loomyHarness) pollOnce(sid string) map[string]interface{} {
out, err := h.ps.InvokeHandler("channels", "poll_login", &lua.HandlerRequest{
Method: "GET", Query: map[string]string{"id": sid}, Role: "admin",
})
if err != nil {
h.t.Fatal(err)
}
return out.JSON.(map[string]interface{})
}
// TestLoomyWechatPollBehavior 逐行锁定微信长轮询的状态判据。
func TestLoomyWechatPollBehavior(t *testing.T) {
h := newLoomyHarness(t)
t.Run("行1 授权页抽出uuid并给出二维码", func(t *testing.T) {
atomic.StoreInt32(&h.polls, 0)
j, _ := h.begin()
qr, _ := j["qr_url"].(string)
if !strings.Contains(qr, "UUID-TEST-1") {
t.Fatalf("二维码 URL 应含抽出的 uuid, 实得 %q", qr)
}
})
t.Run("行2 408等待_404已扫码但不算完成", func(t *testing.T) {
atomic.StoreInt32(&h.polls, 0)
atomic.StoreInt32(&h.confirmAt, 99)
_, sid := h.begin()
// 第一拍 408
if st := h.pollOnce(sid); st["state"] != "pending" {
t.Fatalf("408 应 pending, 实得 %v", st["state"])
}
// 第二拍 404 —— **关键**:已扫码但**不是**完成(早期读反会卡这里)
st := h.pollOnce(sid)
if st["state"] != "pending" {
t.Fatalf("404(已扫码待确认) 必须是 pending 而非 completed, 实得 %v", st["state"])
}
})
t.Run("行3 405才算确认_并换到凭据", func(t *testing.T) {
atomic.StoreInt32(&h.polls, 0)
atomic.StoreInt32(&h.confirmAt, 3)
_, sid := h.begin()
h.pollOnce(sid) // 408
h.pollOnce(sid) // 404
st := h.pollOnce(sid) // 405
if st["state"] != "completed" {
t.Fatalf("405 应 completed, 实得 %v (msg=%v)", st["state"], st["message"])
}
// 账号端点必须用讯飞签名头(account ak:sig),不是 Bearer
if !strings.HasPrefix(h.gotAuth, "account ") {
t.Fatalf("账号端点应用 account ak:sig 签名, 实得 %q", h.gotAuth)
}
if !strings.Contains(h.gotAuth, ":") {
t.Fatalf("签名头应含冒号分隔的 ak:sig, 实得 %q", h.gotAuth)
}
v, ok, _ := h.ps.GetSecret("channels", "channels:loomy")
if !ok || !strings.Contains(v, "RCODE-1") {
t.Fatalf("凭据应含 rcode, 实得 %q", v)
}
})
t.Run("行4 403取消_402失效_都是终态失败", func(t *testing.T) {
for _, tc := range []struct{ errcode, want string }{
{"403", "取消"}, {"402", "失效"},
} {
hh := newLoomyHarness(t)
hh.ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/connect/qrconnect") {
fmt.Fprint(w, `<img src="/connect/qrcode/U">`)
return
}
fmt.Fprintf(w, "window.wx_errcode=%s;", tc.errcode)
})
_, sid := hh.begin()
st := hh.pollOnce(sid)
if st["state"] != "failed" {
t.Fatalf("%s 应 failed, 实得 %v", tc.errcode, st["state"])
}
}
})
}