package delivery_test import ( "context" "errors" "orchestra/internal/delivery" "orchestra/internal/domain" "testing" "time" ) type fakeSender struct { fail bool sent []string } func (f *fakeSender) Send(_ context.Context, msg string) error { if f.fail { return errors.New("boom") } f.sent = append(f.sent, msg) return nil } // TestFanoutContinuesAfterSendError guards S4: a single sender error must // not kill the fanout goroutine or stall the cursor for the other senders. func TestFanoutContinuesAfterSendError(t *testing.T) { failing := &fakeSender{fail: true} ok := &fakeSender{} var gotErrs int var savedCursor uint64 f := &delivery.Fanout{ Senders: []delivery.Sender{failing, ok}, Retry: time.Millisecond, OnError: func(error) { gotErrs++ }, SaveCursor: func(c uint64) { savedCursor = c }, } events := []domain.Event{{Seq: 1, Type: "TaskCompleted", TaskID: "t1"}} served := false ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancel() err := f.Run(ctx, func(cursor uint64) []domain.Event { if served { return nil } served = true return events }) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("Run returned %v, want deadline exceeded (goroutine must survive send errors)", err) } if gotErrs == 0 { t.Fatal("expected OnError to be called for the failing sender") } if len(ok.sent) != 1 { t.Fatalf("healthy sender got %d messages, want 1", len(ok.sent)) } if savedCursor != 1 { t.Fatalf("cursor=%d, want 1 (must advance despite the other sender's error)", savedCursor) } }