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 }