Files
Cyrene_v2/internal/channel/adapter/connect_outcome_test.go
T
AskaEth b4d55f412e
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(channel): 连接动作不再乐观 —— 适配器自报连接态三态 + 面板如实呈现
根因:adminChannelOpen 在 ch.Connect 返回 nil 后硬编码 connected=true。而 Connect 返回 nil 只说明「这次调用没有致命失败」—— ws+client 首连失败刻意非致命(OneBot 实现可能比 Cyrene 后启动),ws+server 则只需监听就绪,两种情况 IsConnected() 都是 false。前端又只按「请求成功」写死「已连接」,于是管理面把「后端受理了」说成「渠道连上了」。

改法:三态口径落在适配器自己身上(connected/pending/ready,internal/channel/adapter 的纯函数 ConnectStateOf 判定 + ConnectSignals 结构化入参,避免两个 bool 接反),qq_obv11 实现 ConnectOutcome 并记住脱敏后的失败原因;网关经类型断言探测(与 accountStatusAware/rescanAware 同套路,核心 sdk.Channel 契约刻意不扩展),只做映射不猜 transport/mode。

线上投影:open/close 响应与 GET /admin/channels 的平台行/账号行各新增 pending/ready/reason 三个可选键(零值即整键缺席,同 NeedsRescan/LastError 口径);既有四键逐字不变,未实现该状态面的平台输出逐字节不变。close 报真实 IsConnected(),断开也不乐观。

凭据边界:reason 三层脱敏(形状级 → 已知明文替换 → 剥 access_token= 查询参数),日志同样只用脱敏串。

验收:主会话亲自复跑 go build/vet 全绿、全树 go test 无 FAIL、目标三包 test 全绿、前端 tsc --noEmit 干净、esbuild 逻辑测试 34+46 checks 全过;主会话亲自变异摘掉第 3 层脱敏实现,发现原两条脱敏用例其实钉不住它(值恰好等于已知明文/被形状级顺带吃掉)⇒ 补一条钉住它独有窗口的用例(短值+紧邻空白),变异下必红、还原后绿。
2026-10-13 19:38:56 +08:00

76 lines
3.4 KiB
Go

// 本文件只测连接态三态的**纯判定**(ConnectStateOf):它不读时钟、不触网、不加锁,
// 因此三态可以逐条单测。适配器侧的落点(qq_obv11 的 ConnectOutcome 与脱敏)见
// plugins/channels/qq_obv11/connect_outcome_test.go,网关侧的映射见
// internal/services/gateway/admin_channel_connect_outcome_test.go。
package adapter
import "testing"
// TestConnectStateOfNotStartedHasNoOpinion 断言未启动的适配器**不编状态**:回空串,
// 调用方据此退回只看 IsConnected(),而不是随便认一个。
func TestConnectStateOfNotStartedHasNoOpinion(t *testing.T) {
t.Parallel()
for _, sig := range []ConnectSignals{
{},
{Connected: true},
{Retrying: true, Reason: "boom"},
{Listening: true},
} {
if state, reason := ConnectStateOf(sig); state != "" || reason != "" {
t.Errorf("ConnectStateOf(%+v) = (%q,%q), want 无意见", sig, state, reason)
}
}
}
// TestConnectStateOfConnectedWins 断言真连上优先于其它分支(监听与重试信号同时为真
// 也不该改变结论),且**不**带原因 —— 「已连接」旁边不该挂一条过期的失败原因。
func TestConnectStateOfConnectedWins(t *testing.T) {
t.Parallel()
state, reason := ConnectStateOf(ConnectSignals{
Started: true, Connected: true, Retrying: true, Listening: true, Reason: "stale failure",
})
if state != ConnectStateConnected || reason != "" {
t.Fatalf("ConnectStateOf = (%q,%q), want (%q,\"\")", state, reason, ConnectStateConnected)
}
}
// TestConnectStateOfRetryingIsPending 断言「未连上但重试在跑」是 pending 且带上原因。
// 这正是 2026-10-07 事故里被写成「已连接」(或静态「未连接」)的那一格。
func TestConnectStateOfRetryingIsPending(t *testing.T) {
t.Parallel()
state, reason := ConnectStateOf(ConnectSignals{
Started: true, Retrying: true, Reason: "dial tcp 127.0.0.1:10301: connection refused",
})
if state != ConnectStatePending {
t.Fatalf("state = %q, want %q", state, ConnectStatePending)
}
if reason != "dial tcp 127.0.0.1:10301: connection refused" {
t.Fatalf("reason = %q, want the failure reason verbatim", reason)
}
// 三个取值互不相同:把 pending 写成 connected 是这次要根除的谎。
if ConnectStatePending == ConnectStateConnected || ConnectStatePending == ConnectStateReady {
t.Fatal("pending 的取值必须与 connected / ready 都不同")
}
}
// TestConnectStateOfListeningIsReady 断言「已绑定监听、等对端接入」是 ready 而不是
// pending:它不是失败,因此**没有**原因可说(哪怕结构体里带着一条陈旧的 reason)。
func TestConnectStateOfListeningIsReady(t *testing.T) {
t.Parallel()
state, reason := ConnectStateOf(ConnectSignals{
Started: true, Listening: true, Reason: "should not be surfaced",
})
if state != ConnectStateReady || reason != "" {
t.Fatalf("ConnectStateOf = (%q,%q), want (%q,\"\")", state, reason, ConnectStateReady)
}
}
// TestConnectStateOfUnexplainedStaysSilent 断言「已启动、未连上、也没有可解释的就绪面」
// 时**不编**状态:既不说「正在连接」(不知道有没有在连),也不说「就绪」。
func TestConnectStateOfUnexplainedStaysSilent(t *testing.T) {
t.Parallel()
if state, reason := ConnectStateOf(ConnectSignals{Started: true, Reason: "orphan"}); state != "" || reason != "" {
t.Fatalf("ConnectStateOf = (%q,%q), want 无意见", state, reason)
}
}