package channelbridge import ( "context" "errors" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/user" ) func newProactiveBridge(t *testing.T, owner *user.OwnerProfile, ad *fakeAdapter) *Bridge { t.Helper() chat := &stubChatter{res: &loop.Result{Text: "hi"}} sess := &stubSessions{sess: session.Session{SessionID: "sess-1", Owner: true, MemWritable: true}} b, err := New(Options{Loop: chat, Sessions: sess, Owner: owner}) if err != nil { t.Fatalf("new bridge: %v", err) } if ad != nil { b.Add(ad) } return b } // proactiveMsg builds a minimal autonomous proactive message as backthink's // buildProactiveMessage would (no channel targeting yet). func proactiveMsg(content string) adapter.UnifiedMessage { return adapter.UnifiedMessage{ Platform: "cyrene-backthink", ChannelType: adapter.ChannelDirect, Content: content, ContentType: adapter.ContentText, Sender: adapter.Actor{Role: adapter.RoleSystem, DisplayName: "Cyrene"}, } } // TestBridge_SendProactive_OwnerChannel verifies a proactive push lands on the // owner's IM channel: the adapter for the owner's platform is selected and the // owner account id is threaded as the DM peer. func TestBridge_SendProactive_OwnerChannel(t *testing.T) { ad := &fakeAdapter{name: "telegram", connected: true} owner := &user.OwnerProfile{Accounts: []user.OwnerAccount{{Platform: "telegram", AccountID: "100"}}} b := newProactiveBridge(t, owner, ad) if err := b.SendProactive(context.Background(), proactiveMsg("记得喝水哦")); err != nil { t.Fatalf("SendProactive err = %v", err) } if len(ad.sends) != 1 { t.Fatalf("sends = %d, want 1", len(ad.sends)) } got := ad.sends[0] if got.Platform != "telegram" { t.Fatalf("platform = %q, want telegram (owner account platform)", got.Platform) } if got.Content != "记得喝水哦" { t.Fatalf("content = %q, want the proactive text", got.Content) } if got.ChannelID != "100" || got.SenderID != "100" { t.Fatalf("peer = %q/%q, want the owner account id 100/100", got.ChannelID, got.SenderID) } if got.ChannelType != adapter.ChannelDirect { t.Fatalf("channel type = %q, want direct", got.ChannelType) } } // TestBridge_SendProactive_PlatformHintSelectsAccount verifies msg.Platform // selects the matching owner account when one exists, rather than defaulting. func TestBridge_SendProactive_PlatformHintSelectsAccount(t *testing.T) { ad := &fakeAdapter{name: "kook", connected: true} owner := &user.OwnerProfile{Accounts: []user.OwnerAccount{ {Platform: "telegram", AccountID: "100"}, {Platform: "kook", AccountID: "329284282"}, }} b := newProactiveBridge(t, owner, ad) msg := proactiveMsg("洗完了") msg.Platform = "kook" if err := b.SendProactive(context.Background(), msg); err != nil { t.Fatalf("SendProactive err = %v", err) } if len(ad.sends) != 1 { t.Fatalf("sends = %d, want 1", len(ad.sends)) } got := ad.sends[0] if got.Platform != "kook" || got.SenderID != "329284282" { t.Fatalf("target = %q/%q, want the kook owner account", got.Platform, got.SenderID) } } // TestBridge_SendProactive_NoOwnerChannel verifies a clearly-worded error when the // owner profile has no whitelisted account, so the caller records the reason. func TestBridge_SendProactive_NoOwnerChannel(t *testing.T) { b := newProactiveBridge(t, &user.OwnerProfile{}, nil) err := b.SendProactive(context.Background(), proactiveMsg("hi")) if err == nil { t.Fatalf("expected an error for a missing owner channel, got nil") } if !strings.Contains(err.Error(), "no owner IM channel") { t.Fatalf("error = %q, want a 'no owner IM channel' note", err) } } // TestBridge_SendProactive_NoAdapter verifies the error names the platform when the // owner has a whitelisted channel but no adapter is registered for it. func TestBridge_SendProactive_NoAdapter(t *testing.T) { owner := &user.OwnerProfile{Accounts: []user.OwnerAccount{{Platform: "kook", AccountID: "329284282"}}} b := newProactiveBridge(t, owner, nil) err := b.SendProactive(context.Background(), proactiveMsg("hi")) if err == nil { t.Fatalf("expected an error for a missing adapter, got nil") } if !strings.Contains(err.Error(), "no adapter for owner platform") { t.Fatalf("error = %q, want a 'no adapter for owner platform' note", err) } } // TestBridge_SendProactive_AdapterError verifies an adapter send failure surfaces // as a wrapped error with the platform (so the record/detail is honest). func TestBridge_SendProactive_AdapterError(t *testing.T) { ad := &fakeAdapter{name: "kook", connected: true, sendErr: errors.New("kook disconnected")} owner := &user.OwnerProfile{Accounts: []user.OwnerAccount{{Platform: "kook", AccountID: "329284282"}}} b := newProactiveBridge(t, owner, ad) err := b.SendProactive(context.Background(), proactiveMsg("hi")) if err == nil { t.Fatalf("expected an error, got nil") } if !strings.Contains(err.Error(), "kook") || !strings.Contains(err.Error(), "kook disconnected") { t.Fatalf("error = %q, want it to wrap the adapter error and name the platform", err) } }