Files
agentic-coding-workflow/internal/infra/notify/stream_test.go
T
2026-06-22 08:55:57 +08:00

121 lines
2.8 KiB
Go

package notify
import (
"context"
"testing"
"time"
"github.com/google/uuid"
)
func TestStreamHubSubscribeReceivesPublish(t *testing.T) {
hub := NewStreamHub()
uid := uuid.New()
ch, cancel := hub.Subscribe(uid)
defer cancel()
want := Message{ID: uuid.New(), UserID: uid, Topic: "t", Title: "hi"}
hub.Publish(uid, want)
select {
case got := <-ch:
if got.ID != want.ID {
t.Fatalf("got %v, want %v", got.ID, want.ID)
}
case <-time.After(time.Second):
t.Fatal("did not receive published message")
}
}
func TestStreamHubUnsubscribeStopsDelivery(t *testing.T) {
hub := NewStreamHub()
uid := uuid.New()
ch, cancel := hub.Subscribe(uid)
cancel() // immediate unsubscribe closes the channel
// channel should be closed, draining returns zero value + !ok.
if _, ok := <-ch; ok {
t.Fatal("expected channel closed after cancel")
}
// Publishing to an unsubscribed user must not panic.
hub.Publish(uid, Message{UserID: uid})
}
func TestStreamHubDropsSlowSubscriber(t *testing.T) {
hub := NewStreamHub()
uid := uuid.New()
ch, cancel := hub.Subscribe(uid)
defer cancel()
// Flood beyond buffer; Publish must not block / deadlock.
done := make(chan struct{})
go func() {
for i := 0; i < streamSubBuffer*4; i++ {
hub.Publish(uid, Message{UserID: uid, Topic: "flood"})
}
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Publish deadlocked on slow subscriber")
}
// Buffer holds at most streamSubBuffer; the rest were dropped.
count := 0
for {
select {
case <-ch:
count++
default:
if count > streamSubBuffer {
t.Fatalf("buffered %d > cap %d (drop-on-slow violated)", count, streamSubBuffer)
}
return
}
}
}
func TestDispatcherPublishesAfterInsert(t *testing.T) {
repo := &fakeRepo{}
d := NewDispatcher(repo, newTestLogger())
hub := NewStreamHub()
d.SetStreamHub(hub)
uid := uuid.New()
ch, cancel := hub.Subscribe(uid)
defer cancel()
if err := d.Dispatch(context.Background(), Message{UserID: uid, Topic: "x", Title: "y"}); err != nil {
t.Fatalf("dispatch: %v", err)
}
select {
case got := <-ch:
if got.Topic != "x" {
t.Fatalf("topic = %q", got.Topic)
}
case <-time.After(time.Second):
t.Fatal("hub did not receive dispatched message")
}
}
func TestDispatcherNoPublishOnInsertFailure(t *testing.T) {
repo := &fakeRepo{failNext: true}
d := NewDispatcher(repo, newTestLogger())
hub := NewStreamHub()
d.SetStreamHub(hub)
uid := uuid.New()
ch, cancel := hub.Subscribe(uid)
defer cancel()
if err := d.Dispatch(context.Background(), Message{UserID: uid}); err == nil {
t.Fatal("expected dispatch error on insert failure")
}
select {
case <-ch:
t.Fatal("hub received message despite insert failure")
case <-time.After(100 * time.Millisecond):
// expected: no publish.
}
}