Files
Cyrene_v2/cmd/cyrene/channels_obv11_wiring_test.go
AskaEth b82870f41e
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(channels): QQ 适配器接上注入 logger(帧取证复活)+ 兜底不再静默降级(Warn 说明 Debug 会被丢)
2026-10-05 12:53:17 +08:00

107 lines
3.9 KiB
Go

package main
// 钉住组合根侧的接线契约:buildAccountAdapter 构造 qq-obv11 适配器时必须把 channelFactory
// 手上的 logger 传进去(与 kook/telegram/webhook 同口径,见 cmd/cyrene/channels.go)。缺陷
// 形态:漏传 ⇒ 适配器自造默认 INFO 兜底 ⇒ 入站帧取证(DEBUG)全部静默丢弃,主人要查的
// 「群友 @ 没回 / 戳一戳没反应」在日志里一片空白,且与「上游压根没上报」无法区分。
//
// 断言走**真实入站路径**:真开一次 http+server 端、真 POST 一条 OneBot 上报,再看注入的
// 捕获器里有没有 `msg="inbound frame"`。所以只要接线一断,这条测试必红。
import (
"bytes"
"context"
"log/slog"
"net/http"
"strings"
"sync"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/qq_obv11"
)
// logSyncBuf 是并发安全的日志捕获目标:上报由 http.Server 自己的 goroutine 处理,断言在
// 测试 goroutine 上读。
type logSyncBuf struct {
mu sync.Mutex
buf bytes.Buffer
}
func (b *logSyncBuf) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.Write(p)
}
func (b *logSyncBuf) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.String()
}
// qqObv11WiringFrame 是一条最普通的群消息事件(与 plugins/channels/qq_obv11 的取证测试
// 同口径):走到 handlePayload 就必然留一行入站帧。
const qqObv11WiringFrame = `{"post_type":"message","message_type":"group","sub_type":"normal",
"self_id":10001,"user_id":20002,"group_id":1070898847,
"raw_message":"hello","message":[{"type":"text","data":{"text":"hello"}}]}`
// TestBuildAccountAdapter_QQObv11WiresLoggerIntoInboundForensics 断言注入的 logger 真的
// 到了适配器手里。捕获器级别设为 DEBUG:取证日志本身是 DEBUG 级,用默认 INFO 的捕获器
// 「丢了」与「没丢」都看不见,测不出这条接线。
func TestBuildAccountAdapter_QQObv11WiresLoggerIntoInboundForensics(t *testing.T) {
buf := &logSyncBuf{}
log := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
acct := config.ChannelAccount{Config: map[string]any{
"transport": "http",
"mode": "server",
"listen_addr": "127.0.0.1:0",
"report_path": "/onebot/report",
}}
built, err := buildAccountAdapter("qq-obv11", acct, log)
if err != nil {
t.Fatalf("buildAccountAdapter: %v", err)
}
ctx := context.Background()
if cerr := built.Connect(ctx); cerr != nil {
t.Fatalf("Connect: %v", cerr)
}
t.Cleanup(func() { _ = built.Disconnect(context.Background()) })
post := "http://" + qqObv11ListenAddr(t, built) + "/onebot/report"
resp, perr := http.Post(post, "application/json", strings.NewReader(qqObv11WiringFrame))
if perr != nil {
t.Fatalf("POST %s: %v", post, perr)
}
if cerr := resp.Body.Close(); cerr != nil {
t.Fatalf("close response body: %v", cerr)
}
if got := buf.String(); !strings.Contains(got, `msg="inbound frame"`) {
t.Fatalf("注入的 logger 没收到入站帧日志——组合根没把 logger 接进 qq-obv11 适配器?\n捕获到的日志:\n%s", got)
}
if got := buf.String(); !strings.Contains(got, "source=plugins/channels/qq_obv11") {
t.Errorf("入站帧日志缺来源标注:\n%s", got)
}
}
// qqObv11ListenAddr 取出适配器实际绑定的监听地址(listen_addr 用 :0 让内核挑端口)。
func qqObv11ListenAddr(t *testing.T, built adapter.PlatformAdapter) string {
t.Helper()
named, ok := built.(*namedAdapter)
if !ok {
t.Fatalf("适配器类型 = %T, want *namedAdapter", built)
}
inner, ok := named.PlatformAdapter.(*qq_obv11.Adapter)
if !ok {
t.Fatalf("内层适配器类型 = %T, want *qq_obv11.Adapter", named.PlatformAdapter)
}
addr := inner.Addr()
if addr == "" {
t.Fatal("适配器没绑定监听地址:http+server 端没起来")
}
return addr
}