@@ -0,0 +1,157 @@
10+ tea "charm.land/bubbletea/v2"
11+ "github.com/charmbracelet/crush/internal/pubsub"
12+ "github.com/stretchr/testify/require"
16+func TestSetupSubscriber_NormalFlow(t *testing.T) {
17+ synctest.Test(t, func(t *testing.T) {
18+ f := newSubscriberFixture(t, 10)
20+ time.Sleep(10 * time.Millisecond)
23+ f.broker.Publish(pubsub.CreatedEvent, "event1")
24+ f.broker.Publish(pubsub.CreatedEvent, "event2")
29+ case <-time.After(5 * time.Second):
30+ t.Fatal("Timed out waiting for messages")
39+func TestSetupSubscriber_SlowConsumer(t *testing.T) {
40+ synctest.Test(t, func(t *testing.T) {
41+ f := newSubscriberFixture(t, 0)
45+ var pubWg sync.WaitGroup
47+ for range numEvents {
48+ f.broker.Publish(pubsub.CreatedEvent, "event")
49+ time.Sleep(10 * time.Millisecond)
54+ time.Sleep(time.Duration(numEvents) * (subscriberSendTimeout + 20*time.Millisecond))
66+ require.Less(t, received, numEvents, "Slow consumer should have dropped some messages")
73+func TestSetupSubscriber_ContextCancellation(t *testing.T) {
74+ synctest.Test(t, func(t *testing.T) {
75+ f := newSubscriberFixture(t, 10)
77+ f.broker.Publish(pubsub.CreatedEvent, "event1")
78+ time.Sleep(100 * time.Millisecond)
86+func TestSetupSubscriber_DrainAfterDrop(t *testing.T) {
87+ synctest.Test(t, func(t *testing.T) {
88+ f := newSubscriberFixture(t, 0)
90+ time.Sleep(10 * time.Millisecond)
93+ // First event: nobody reads outputCh so the timer fires (message dropped).
94+ f.broker.Publish(pubsub.CreatedEvent, "event1")
95+ time.Sleep(subscriberSendTimeout + 25*time.Millisecond)
98+ // Second event: triggers Stop()==false path; without the fix this deadlocks.
99+ f.broker.Publish(pubsub.CreatedEvent, "event2")
101+ // If the timer drain deadlocks, wg.Wait never returns.
102+ done := make(chan struct{})
111+ case <-time.After(5 * time.Second):
112+ t.Fatal("setupSubscriber goroutine hung — likely timer drain deadlock")
117+func TestSetupSubscriber_NoTimerLeak(t *testing.T) {
118+ defer goleak.VerifyNone(t)
119+ synctest.Test(t, func(t *testing.T) {
120+ f := newSubscriberFixture(t, 100)
123+ f.broker.Publish(pubsub.CreatedEvent, "event")
124+ time.Sleep(5 * time.Millisecond)
133+type subscriberFixture struct {
134+ broker *pubsub.Broker[string]
136+ outputCh chan tea.Msg
137+ cancel context.CancelFunc
140+func newSubscriberFixture(t *testing.T, bufSize int) *subscriberFixture {
142+ ctx, cancel := context.WithCancel(t.Context())
145+ f := &subscriberFixture{
146+ broker: pubsub.NewBroker[string](),
147+ outputCh: make(chan tea.Msg, bufSize),
150+ t.Cleanup(f.broker.Shutdown)
152+ setupSubscriber(ctx, &f.wg, "test", func(ctx context.Context) <-chan pubsub.Event[string] {
153+ return f.broker.Subscribe(ctx)