package channelbridge import ( "context" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/user" ) // gapCounter 返回一个 bubbleGapFn:它统计气泡间延时的咨询次数 // 并返回零,因此无需为每个间隔真的 sleep 0.5-2s // 就能断言多气泡间隔。 func gapCounter(counter *int) func() time.Duration { return func() time.Duration { *counter++ return 0 } } func TestSendIMBubbles_SplitsStripsAndGaps(t *testing.T) { t.Parallel() ad := &fakeAdapter{name: "kook", connected: true} b, err := New(Options{Loop: &stubChatter{}, Sessions: &stubSessions{}}) if err != nil { t.Fatalf("New: %v", err) } gaps := 0 b.bubbleGapFn = gapCounter(&gaps) target := adapter.UnifiedMessage{ Platform: "kook", ChannelID: "329284282", ChannelType: adapter.ChannelDirect, SenderID: "329284282", } content := "

弯起眼睛笑了当然看得到呀。

第二句话。

" if err := b.sendIMBubbles(context.Background(), target, func(ctx context.Context, m adapter.UnifiedMessage) error { return ad.SendMessage(ctx, adapter.UnifiedResponse{Messages: []adapter.UnifiedMessage{m}}) }, content); err != nil { t.Fatalf("sendIMBubbles err = %v", err) } if len(ad.sends) != 2 { t.Fatalf("sends = %d, want 2

bubbles", len(ad.sends)) } if ad.sends[0].Content != "当然看得到呀。" { t.Fatalf("bubble[0] = %q, want the role-play-stripped text", ad.sends[0].Content) } if ad.sends[1].Content != "第二句话。" { t.Fatalf("bubble[1] = %q, want the second paragraph", ad.sends[1].Content) } if ad.sends[0].Platform != "kook" || ad.sends[0].ChannelID != "329284282" { t.Fatalf("bubble[0] target = %+v, want kook 329284282", ad.sends[0]) } // 两个气泡 → 恰好咨询一次气泡间延时(回复与主动推送两条路径 // 都适用,因为驱动的是共享派发器)。 if gaps != 1 { t.Fatalf("gap calls = %d, want 1 (2 bubbles → one gap)", gaps) } } func TestSendIMBubbles_SingleBubbleNoGap(t *testing.T) { t.Parallel() ad := &fakeAdapter{name: "weixin-ilink", connected: true} b, err := New(Options{Loop: &stubChatter{}, Sessions: &stubSessions{}}) if err != nil { t.Fatalf("New: %v", err) } gaps := 0 b.bubbleGapFn = gapCounter(&gaps) target := adapter.UnifiedMessage{ Platform: "weixin-ilink", ChannelID: "o9cq@im.wechat", ChannelType: adapter.ChannelDirect, SenderID: "o9cq@im.wechat", } if err := b.sendIMBubbles(context.Background(), target, func(ctx context.Context, m adapter.UnifiedMessage) error { return ad.SendProactive(ctx, m) }, "记得喝水哦"); err != nil { t.Fatalf("sendIMBubbles err = %v", err) } if len(ad.sends) != 1 { t.Fatalf("sends = %d, want 1 bubble", len(ad.sends)) } if gaps != 0 { t.Fatalf("gap calls = %d, want 0 for a single bubble", gaps) } } func TestSendIMBubbles_PureActionDropped(t *testing.T) { t.Parallel() ad := &fakeAdapter{name: "kook", connected: true} b, err := New(Options{Loop: &stubChatter{}, Sessions: &stubSessions{}}) if err != nil { t.Fatalf("New: %v", err) } target := adapter.UnifiedMessage{ Platform: "kook", ChannelID: "1", ChannelType: adapter.ChannelDirect, SenderID: "1", } if err := b.sendIMBubbles(context.Background(), target, func(ctx context.Context, m adapter.UnifiedMessage) error { return ad.SendMessage(ctx, adapter.UnifiedResponse{Messages: []adapter.UnifiedMessage{m}}) }, "拍拍你"); err != nil { t.Fatalf("sendIMBubbles err = %v", err) } if len(ad.sends) != 0 { t.Fatalf("sends = %d, want 0 for a pure action line", len(ad.sends)) } } // TestSendReply_SplitsBubblesAndStrips 覆盖重构后的回复出口(sendReply): // 含多个

的回复经共享派发器以多个已剥离的气泡 // 投递,目标为来源会话。 func TestSendReply_SplitsBubblesAndStrips(t *testing.T) { t.Parallel() b, _, _, ad := newTestBridge(t) b.bubbleGapFn = func() time.Duration { return 0 } in := directMessage() content := "

弯起眼睛笑了当然看得到呀。

怎么突然这么问。

" if err := b.sendReply(context.Background(), in, ad, content); err != nil { t.Fatalf("sendReply err = %v", err) } if len(ad.sends) != 2 { t.Fatalf("sends = %d, want 2 (reply splits into

bubbles)", len(ad.sends)) } if ad.sends[0].Content != "当然看得到呀。" || ad.sends[1].Content != "怎么突然这么问。" { t.Fatalf("bubbles = %#v, want stripped narration + both paragraphs", ad.sends) } if ad.sends[0].Platform != "telegram" || ad.sends[0].ChannelID != "100" { t.Fatalf("reply target = %+v, want telegram channel 100", ad.sends[0]) } } // TestSendProactive_SplitsWeChatBubbles 验证主动推送现在会在 WeChat 上 // 把多段消息拆成气泡(上报的回归问题:WeChat // 主动推送曾以一整块未拆分的

到达)。 func TestSendProactive_SplitsWeChatBubbles(t *testing.T) { t.Parallel() const acct = "o9cq802QPKPscBenJAp-SMZPBlnQ@im.wechat" weixin := &fakeAdapter{name: "weixin-ilink", connected: true} owner := &user.OwnerProfile{Accounts: []user.OwnerAccount{{Platform: "weixin-ilink", AccountID: acct}}} b := newProactiveBridge(t, owner, weixin) b.bubbleGapFn = func() time.Duration { return 0 } msg := proactiveMsg("

第一段正文。

拍拍第二段。

") if err := b.SendProactive(context.Background(), msg); err != nil { t.Fatalf("SendProactive err = %v", err) } if len(weixin.sends) != 2 { t.Fatalf("weixin sends = %d, want 2 (proactive splits into

bubbles)", len(weixin.sends)) } if weixin.sends[0].Content != "第一段正文。" || weixin.sends[1].Content != "第二段。" { t.Fatalf("weixin bubbles = %#v, want stripped narration + both paragraphs", weixin.sends) } if weixin.sends[0].Platform != "weixin-ilink" || weixin.sends[0].ChannelID != acct || weixin.sends[0].SenderID != acct { t.Fatalf("weixin peer = %+v, want platform weixin-ilink account %s", weixin.sends[0], acct) } }