mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
78fd080189
Add a non-blocking runtime publish path and switch hot-path publishers to it. Enforce subscription timeout boundaries, keep ordered subscriber snapshots up to date on subscribe changes, expose all runtime kinds to process hooks, add safe log attrs for non-agent events, and close the gateway message bus on full shutdown.
255 lines
6.4 KiB
Go
255 lines
6.4 KiB
Go
package events
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPublishDeliversToMatchingSubscriber(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
bus := NewBus()
|
|
defer closeBus(t, bus)
|
|
|
|
_, ch, err := bus.Channel().OfKind(KindAgentTurnStart).SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "turn-starts", Buffer: 1},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan failed: %v", err)
|
|
}
|
|
|
|
unmatched := bus.Publish(context.Background(), Event{Kind: KindAgentTurnEnd})
|
|
if unmatched.Matched != 0 || unmatched.Delivered != 0 {
|
|
t.Fatalf("unmatched Publish = %+v, want no delivery", unmatched)
|
|
}
|
|
|
|
result := bus.Publish(context.Background(), Event{Kind: KindAgentTurnStart})
|
|
if result.Matched != 1 || result.Delivered != 1 || result.Dropped != 0 {
|
|
t.Fatalf("Publish = %+v, want one delivered event", result)
|
|
}
|
|
|
|
evt := receiveEvent(t, ch)
|
|
if evt.Kind != KindAgentTurnStart {
|
|
t.Fatalf("event kind = %q, want %q", evt.Kind, KindAgentTurnStart)
|
|
}
|
|
if evt.ID == "" {
|
|
t.Fatal("event ID is empty")
|
|
}
|
|
if evt.Time.IsZero() {
|
|
t.Fatal("event Time is zero")
|
|
}
|
|
}
|
|
|
|
func TestDropNewestIncrementsStats(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
bus := NewBus()
|
|
defer closeBus(t, bus)
|
|
|
|
sub, _, err := bus.Channel().SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "drop-newest", Buffer: 1, Backpressure: DropNewest},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan failed: %v", err)
|
|
}
|
|
|
|
first := bus.Publish(context.Background(), Event{Kind: KindAgentTurnStart})
|
|
if first.Delivered != 1 || first.Dropped != 0 {
|
|
t.Fatalf("first Publish = %+v, want one delivered event", first)
|
|
}
|
|
|
|
second := bus.Publish(context.Background(), Event{Kind: KindAgentTurnEnd})
|
|
if second.Delivered != 0 || second.Dropped != 1 {
|
|
t.Fatalf("second Publish = %+v, want one dropped event", second)
|
|
}
|
|
|
|
if got := sub.Stats().Dropped; got != 1 {
|
|
t.Fatalf("subscription dropped = %d, want 1", got)
|
|
}
|
|
if got := bus.Stats().Dropped; got != 1 {
|
|
t.Fatalf("bus dropped = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestDropOldestKeepsNewestEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
bus := NewBus()
|
|
defer closeBus(t, bus)
|
|
|
|
sub, ch, err := bus.Channel().SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "drop-oldest", Buffer: 1, Backpressure: DropOldest},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan failed: %v", err)
|
|
}
|
|
|
|
bus.Publish(context.Background(), Event{Kind: Kind("test.old"), Payload: "old"})
|
|
result := bus.Publish(context.Background(), Event{Kind: Kind("test.new"), Payload: "new"})
|
|
if result.Delivered != 1 || result.Dropped != 1 {
|
|
t.Fatalf("Publish = %+v, want replacement delivery", result)
|
|
}
|
|
|
|
evt := receiveEvent(t, ch)
|
|
if evt.Payload != "new" {
|
|
t.Fatalf("payload = %v, want new", evt.Payload)
|
|
}
|
|
if got := sub.Stats().Dropped; got != 1 {
|
|
t.Fatalf("subscription dropped = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestBlockRespectsContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
bus := NewBus()
|
|
defer closeBus(t, bus)
|
|
|
|
_, _, err := bus.Channel().SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "block", Buffer: 1, Backpressure: Block},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan failed: %v", err)
|
|
}
|
|
|
|
first := bus.Publish(context.Background(), Event{Kind: Kind("test.first")})
|
|
if first.Delivered != 1 {
|
|
t.Fatalf("first Publish = %+v, want one delivered event", first)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
|
|
second := bus.Publish(ctx, Event{Kind: Kind("test.second")})
|
|
if second.Blocked != 1 || second.Dropped != 1 || second.Delivered != 0 {
|
|
t.Fatalf("second Publish = %+v, want one blocked drop", second)
|
|
}
|
|
}
|
|
|
|
func TestPublishNonBlockingDropsForFullBlockSubscriber(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
bus := NewBus()
|
|
defer closeBus(t, bus)
|
|
|
|
sub, _, err := bus.Channel().SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "block", Buffer: 1, Backpressure: Block},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan failed: %v", err)
|
|
}
|
|
|
|
first := bus.PublishNonBlocking(Event{Kind: Kind("test.first")})
|
|
if first.Delivered != 1 {
|
|
t.Fatalf("first PublishNonBlocking = %+v, want one delivered event", first)
|
|
}
|
|
|
|
resultCh := make(chan PublishResult, 1)
|
|
go func() {
|
|
resultCh <- bus.PublishNonBlocking(Event{Kind: Kind("test.second")})
|
|
}()
|
|
|
|
select {
|
|
case second := <-resultCh:
|
|
if second.Matched != 1 || second.Delivered != 0 || second.Dropped != 1 || second.Blocked != 0 {
|
|
t.Fatalf("second PublishNonBlocking = %+v, want non-blocking drop", second)
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("PublishNonBlocking blocked on full Block subscriber")
|
|
}
|
|
|
|
if got := sub.Stats().Dropped; got != 1 {
|
|
t.Fatalf("subscription dropped = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestStatsSubscribersKeepPriorityOrder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
bus := NewBus()
|
|
defer closeBus(t, bus)
|
|
|
|
low, _, err := bus.Channel().SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "low", Priority: -1},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan low failed: %v", err)
|
|
}
|
|
high, _, err := bus.Channel().SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "high", Priority: 10},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan high failed: %v", err)
|
|
}
|
|
peer, _, err := bus.Channel().SubscribeChan(
|
|
context.Background(),
|
|
SubscribeOptions{Name: "peer", Priority: 10},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan peer failed: %v", err)
|
|
}
|
|
|
|
stats := bus.Stats()
|
|
got := []string{
|
|
stats.SubscriberStats[0].Name,
|
|
stats.SubscriberStats[1].Name,
|
|
stats.SubscriberStats[2].Name,
|
|
}
|
|
want := []string{"high", "peer", "low"}
|
|
if got[0] != want[0] || got[1] != want[1] || got[2] != want[2] {
|
|
t.Fatalf("subscriber order = %v, want %v", got, want)
|
|
}
|
|
|
|
if err := high.Close(); err != nil {
|
|
t.Fatalf("Close high failed: %v", err)
|
|
}
|
|
|
|
stats = bus.Stats()
|
|
got = []string{
|
|
stats.SubscriberStats[0].Name,
|
|
stats.SubscriberStats[1].Name,
|
|
}
|
|
want = []string{"peer", "low"}
|
|
if got[0] != want[0] || got[1] != want[1] {
|
|
t.Fatalf("subscriber order after unsubscribe = %v, want %v", got, want)
|
|
}
|
|
|
|
if err := peer.Close(); err != nil {
|
|
t.Fatalf("Close peer failed: %v", err)
|
|
}
|
|
if err := low.Close(); err != nil {
|
|
t.Fatalf("Close low failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func receiveEvent(t *testing.T, ch <-chan Event) Event {
|
|
t.Helper()
|
|
|
|
select {
|
|
case evt, ok := <-ch:
|
|
if !ok {
|
|
t.Fatal("event channel closed before receive")
|
|
}
|
|
return evt
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for event")
|
|
return Event{}
|
|
}
|
|
}
|
|
|
|
func closeBus(t *testing.T, bus *EventBus) {
|
|
t.Helper()
|
|
|
|
if err := bus.Close(); err != nil {
|
|
t.Fatalf("Close failed: %v", err)
|
|
}
|
|
}
|