package acp_test import ( "context" "sync" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/acp" ) func TestTurnBus_FanoutToMultipleSubscribers(t *testing.T) { t.Parallel() bus := acp.NewTurnBus(nil) var wg sync.WaitGroup wg.Add(2) var a, b atomic.Int32 bus.Subscribe("a", func(_ context.Context, _ acp.TurnEvent) { a.Add(1); wg.Done() }) bus.Subscribe("b", func(_ context.Context, _ acp.TurnEvent) { b.Add(1); wg.Done() }) bus.Publish(context.Background(), acp.TurnEvent{SessionID: uuid.New(), Kind: acp.TurnCompletedEvent}) done := make(chan struct{}) go func() { wg.Wait(); close(done) }() select { case <-done: case <-time.After(2 * time.Second): t.Fatal("subscribers not invoked in time") } assert.Equal(t, int32(1), a.Load()) assert.Equal(t, int32(1), b.Load()) } func TestTurnBus_PanicIsolatedAndNonBlocking(t *testing.T) { t.Parallel() bus := acp.NewTurnBus(nil) var good atomic.Int32 var wg sync.WaitGroup wg.Add(1) bus.Subscribe("panicky", func(_ context.Context, _ acp.TurnEvent) { panic("boom") }) bus.Subscribe("good", func(_ context.Context, _ acp.TurnEvent) { good.Add(1); wg.Done() }) // Publish 必须立即返回(非阻塞),且 panicky 订阅者的 panic 被 recover 隔离, // 不影响 good 订阅者执行。 bus.Publish(context.Background(), acp.TurnEvent{Kind: acp.SessionIdleEvent}) done := make(chan struct{}) go func() { wg.Wait(); close(done) }() select { case <-done: case <-time.After(2 * time.Second): t.Fatal("good subscriber not invoked despite panicky peer") } assert.Equal(t, int32(1), good.Load()) } func TestTurnBus_PublishDoesNotBlockOnSlowSubscriber(t *testing.T) { t.Parallel() bus := acp.NewTurnBus(nil) release := make(chan struct{}) bus.Subscribe("slow", func(_ context.Context, _ acp.TurnEvent) { <-release }) start := time.Now() bus.Publish(context.Background(), acp.TurnEvent{Kind: acp.SessionIdleEvent}) // Publish 应远快于订阅者的阻塞时长。 require.Less(t, time.Since(start), 500*time.Millisecond, "Publish must not block on slow subscriber") close(release) }