- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避) - 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
157 lines
5.9 KiB
Go
157 lines
5.9 KiB
Go
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 := "<p><action>弯起眼睛笑了</action>当然看得到呀。</p><p>第二句话。</p>"
|
|
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 <p> 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}})
|
|
}, "<action>拍拍你</action>"); 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):
|
|
// 含多个 <p> 的回复经共享派发器以多个已剥离的气泡
|
|
// 投递,目标为来源会话。
|
|
func TestSendReply_SplitsBubblesAndStrips(t *testing.T) {
|
|
t.Parallel()
|
|
b, _, _, ad := newTestBridge(t)
|
|
b.bubbleGapFn = func() time.Duration { return 0 }
|
|
in := directMessage()
|
|
content := "<p><action>弯起眼睛笑了</action>当然看得到呀。</p><p>怎么突然这么问。</p>"
|
|
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 <p> 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
|
|
// 主动推送曾以一整块未拆分的 <p> 到达)。
|
|
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("<p>第一段正文。</p><p><action>拍拍</action>第二段。</p>")
|
|
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 <p> 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)
|
|
}
|
|
}
|