package backthink import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) // TestBackthink_SetActiveSink_RebuildsPusher verifies the sink setters rebuild the // pusher so a later push routes to the injected active client instead of the // default nil-sink RouteNone pusher built at New. func TestBackthink_SetActiveSink_RebuildsPusher(t *testing.T) { db := newTestDB(t) bt, err := New(Config{}, Deps{ Thinker: &mockThinker{text: "[我决定:push]\n[渠道:web]\nhi"}, State: &mockState{}, Gate: &mockGate{allow: true}, DB: db, }) if err != nil { t.Fatalf("new: %v", err) } active := &mockActive{n: 2} im := &mockIMAdapter{} bt.SetActiveSink(active) bt.SetIMSink(im) res := bt.pusher.Push(context.Background(), PushRequest{ Message: adapter.UnifiedMessage{Content: "hi"}, ChannelHint: "web", }) if !res.Pushed || res.Route != RouteActiveClient { t.Fatalf("push = %+v, want active_client pushed", res) } if active.calls != 1 { t.Fatalf("active calls = %d, want 1", active.calls) } if im.count() != 0 { t.Fatalf("im calls = %d, want 0 (active preferred)", im.count()) } } // TestBackthink_SetIMSink_Fallback verifies a wired IM sink receives the push when // no active client is online (so the owner is still reached). func TestBackthink_SetIMSink_Fallback(t *testing.T) { db := newTestDB(t) bt, err := New(Config{}, Deps{ Thinker: &mockThinker{text: "[我决定:push]\nhi"}, State: &mockState{}, Gate: &mockGate{allow: true}, DB: db, }) if err != nil { t.Fatalf("new: %v", err) } active := &mockActive{n: 0} // no active client online im := &mockIMAdapter{} bt.SetActiveSink(active) bt.SetIMSink(im) res := bt.pusher.Push(context.Background(), PushRequest{ Message: adapter.UnifiedMessage{Content: "hi"}, }) if !res.Pushed || res.Route != RouteIMBridge { t.Fatalf("push = %+v, want im_bridge pushed", res) } if im.count() != 1 { t.Fatalf("im calls = %d, want 1", im.count()) } } // TestBackthink_SinkSetters_ConcurrentSafe verifies the setter and the read of the // current pusher do not race under the lifecycle lock, so an in-flight pass never // touches a half-rebuilt pusher. func TestBackthink_SinkSetters_ConcurrentSafe(t *testing.T) { db := newTestDB(t) bt, err := New(Config{}, Deps{ Thinker: &mockThinker{}, State: &mockState{}, Gate: &mockGate{allow: true}, DB: db, }) if err != nil { t.Fatalf("new: %v", err) } done := make(chan struct{}) go func() { defer close(done) for i := 0; i < 200; i++ { bt.SetActiveSink(&mockActive{n: 1}) bt.SetIMSink(&mockIMAdapter{}) } }() for i := 0; i < 200; i++ { _ = bt.currentPusher().Push(context.Background(), PushRequest{Message: adapter.UnifiedMessage{Content: "x"}}) } <-done }