package core import ( "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "llmsproxy/internal/lua" ) // readBundledChannels 读内置 channels 插件源(供行为/变异测试共用)。 func readBundledChannels(t *testing.T) (string, error) { t.Helper() return lua.ReadBundledPlugin("channels") } // deviceHarness 是 device-poll 决策表的可复用测试台:假上游(脚本化逐次响应) // + channels 插件加载指定源码 + minimax 端点指到假上游。 // 行为测试(TestChannelsDevicePollBehavior)与变异测试共用它 —— 决策表只有 // 一份,两处不会漂移。 type deviceHarness struct { t *testing.T ps *lua.Plugins ts *httptest.Server step int32 // script 持续被 HTTP handler 原子读取 script atomic.Value // []scriptStep } type scriptStep struct { http int body string } func newDeviceHarness(t *testing.T, src string) *deviceHarness { t.Helper() h := &deviceHarness{t: t} h.script.Store([]scriptStep{}) h.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/oauth2/device/code": w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{"device_code":"DEV1","user_code":"ABCD-EFGH","verification_uri":"https://example/activate","interval":1,"expires_in":600}`) case "/oauth2/token": i := int(atomic.AddInt32(&h.step, 1)) - 1 script := h.script.Load().([]scriptStep) w.Header().Set("Content-Type", "application/json") if i >= len(script) { w.WriteHeader(500) fmt.Fprint(w, `{"error":"unexpected"}`) return } if script[i].http != 0 && script[i].http != 200 { w.WriteHeader(script[i].http) } fmt.Fprint(w, script[i].body) default: w.WriteHeader(404) } })) t.Cleanup(h.ts.Close) c := kernelTestCore(t) h.ps = c.Plugins() if err := h.ps.LoadSource("channels", src); err != nil { t.Fatalf("加载: %v", err) } if _, err := h.ps.Call("channels", "set_endpoints", map[string]interface{}{ "platform": "minimax", "device": map[string]interface{}{ "code_url": h.ts.URL + "/oauth2/device/code", "token_url": h.ts.URL + "/oauth2/token", }, }); err != nil { t.Fatalf("set_endpoints: %v", err) } return h } // setScript 设置 token 端点的逐次响应脚本。`400 {...}` 形式表示非 200。 func (h *deviceHarness) setScript(steps ...string) { var out []scriptStep for _, b := range steps { code := 200 switch { case strings.HasPrefix(b, "400 "): code, b = 400, b[4:] case strings.HasPrefix(b, "403 "): code, b = 403, b[4:] } out = append(out, scriptStep{http: code, body: b}) } h.script.Store(out) atomic.StoreInt32(&h.step, 0) } func (h *deviceHarness) begin() map[string]interface{} { out, err := h.ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"minimax"}`, }) if err != nil { h.t.Fatal(err) } return out.JSON.(map[string]interface{}) } func (h *deviceHarness) poll(id string) map[string]interface{} { out, _ := h.ps.InvokeHandler("channels", "poll_login", &lua.HandlerRequest{ Method: "GET", Query: map[string]string{"id": id}, }) return out.JSON.(map[string]interface{}) } // runDecisionTable 跑完整 8 行决策表,全部符合预期返回 true。 // 这是行为契约的单一事实来源:行为测试用它断言通过;变异测试用它杀变异体。 func (h *deviceHarness) runDecisionTable() bool { ok := func(cond bool) bool { return cond } st := func(steps ...string) map[string]interface{} { h.setScript(steps...) j := h.begin() sid := j["session"].(map[string]interface{})["id"].(string) return h.poll(sid) } // 行1: 200+pending → 等 if j := st(`{"status":"pending"}`); j["state"] != "pending" { return false } // 行2: 200+slow_down → 等 + interval+5 h.setScript(`{"status":"slow_down"}`) j := h.begin() sid := j["session"].(map[string]interface{})["id"].(string) if j2 := h.poll(sid); j2["state"] != "pending" { return false } else if iv, _ := j2["interval"].(float64); iv < 6 { return false } // 行3: 200+denied → failed,message 区分于通用错误(杀"denied 不识别"变异体) if j := st(`{"status":"denied"}`); j["state"] != "failed" { return false } else if msg, _ := j["message"].(string); !strings.Contains(msg, "用户拒绝") { return false } // 行4: 200+expired_token → failed,message 带过期字样 if j := st(`{"status":"expired_token"}`); j["state"] != "failed" { return false } else if msg, _ := j["message"].(string); !strings.Contains(msg, "过期") { return false } // 行5: 400+authorization_pending → 等 if j := st(`400 {"error":"authorization_pending"}`); j["state"] != "pending" { return false } // 行6: 403+slow_down → 等 + interval+5 h.setScript(`403 {"error":"slow_down"}`) j = h.begin() sid = j["session"].(map[string]interface{})["id"].(string) if j2 := h.poll(sid); j2["state"] != "pending" { return false } else if iv, _ := j2["interval"].(float64); iv < 6 { return false } // 行7: 200+access_token → completed(需先授权 add_source) _ = h.ps.SetPluginKernelPermission("channels", []string{"add_source", "status", "list_sources"}, false) h.setScript(`{"access_token":"MM-ACCESS-1","refresh_token":"MM-RF-1","nickname":"mm-user"}`) j = h.begin() sid = j["session"].(map[string]interface{})["id"].(string) if j2 := h.poll(sid); j2["state"] != "completed" || j2["account"] != "mm-user" { return false } // 行8: 其它 error → failed 带文本 if j := st(`{"error":"server_error"}`); j["state"] != "failed" { return false } return ok(true) }