multica/server/internal/events/bus_test.go
Naiyuan Qing 9236674667 feat(realtime): WS invalidation + refetch pattern, inbox bugfixes, UI polish
Refactor real-time sync from per-event precise mutations to WS-as-invalidation-signal + debounced refetch.

Backend:
- Add SubscribeAll to Event Bus — auto-broadcasts ALL events, eliminates manual 25-item allEvents list
- Add skill event constants to protocol, fix skill handler string literals
- Add title_changed activity tracking

Frontend:
- WSClient: add onAny() method for wildcard event subscription
- useRealtimeSync: rewrite to refreshMap + prefix routing + 100ms debounce
- Precise handlers only for side effects: workspace:deleted, member:removed, member:added (self-check)
- Reconnect now refetches all stores (fixes missing members/skills/workspace refresh)
- Stale-while-revalidate: fetch() only shows loading spinner on initial load, not on refetch
- Remove redundant useWSEvent in agents/page.tsx and skills-page.tsx
- WSClient.disconnect() now clears all handler registrations

Inbox bugfixes:
- Unify sidebar badge count with page count via dedupedItems + unreadCount in store
- Sort by time DESC (removed severity-first ordering)
- Ellipsis on truncated detail labels

UI:
- Status/Priority pickers: replace RadioGroup with MenuItem for auto-close on selection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:49:40 +08:00

146 lines
3.1 KiB
Go

package events
import (
"sync/atomic"
"testing"
)
func TestPublishDeliversToSubscribers(t *testing.T) {
bus := New()
var count int32
bus.Subscribe("test:event", func(e Event) {
atomic.AddInt32(&count, 1)
})
bus.Subscribe("test:event", func(e Event) {
atomic.AddInt32(&count, 1)
})
bus.Publish(Event{Type: "test:event", Payload: "hello"})
if count != 2 {
t.Errorf("expected 2 handlers called, got %d", count)
}
}
func TestPublishOnlyMatchingType(t *testing.T) {
bus := New()
var called bool
bus.Subscribe("type:a", func(e Event) {
called = true
})
bus.Publish(Event{Type: "type:b"})
if called {
t.Error("handler for type:a should not be called for type:b event")
}
}
func TestPublishNoSubscribersIsNoop(t *testing.T) {
bus := New()
// Should not panic
bus.Publish(Event{Type: "no:listeners"})
}
func TestPanicInHandlerDoesNotBreakOthers(t *testing.T) {
bus := New()
var secondCalled bool
bus.Subscribe("test:panic", func(e Event) {
panic("handler panic")
})
bus.Subscribe("test:panic", func(e Event) {
secondCalled = true
})
bus.Publish(Event{Type: "test:panic"})
if !secondCalled {
t.Error("second handler should still be called after first panics")
}
}
func TestSubscribeAllReceivesAllEventTypes(t *testing.T) {
bus := New()
var received []string
bus.SubscribeAll(func(e Event) {
received = append(received, e.Type)
})
bus.Publish(Event{Type: "issue:created"})
bus.Publish(Event{Type: "comment:deleted"})
bus.Publish(Event{Type: "skill:updated"})
if len(received) != 3 {
t.Fatalf("expected 3 events, got %d", len(received))
}
if received[0] != "issue:created" || received[1] != "comment:deleted" || received[2] != "skill:updated" {
t.Fatalf("unexpected events: %v", received)
}
}
func TestSubscribeAllCalledAfterTypeSpecific(t *testing.T) {
bus := New()
var order []string
bus.Subscribe("issue:created", func(e Event) {
order = append(order, "specific")
})
bus.SubscribeAll(func(e Event) {
order = append(order, "global")
})
bus.Publish(Event{Type: "issue:created"})
if len(order) != 2 || order[0] != "specific" || order[1] != "global" {
t.Fatalf("expected [specific, global], got %v", order)
}
}
func TestSubscribeAllPanicRecovery(t *testing.T) {
bus := New()
var secondCalled bool
bus.SubscribeAll(func(e Event) {
panic("test panic")
})
bus.SubscribeAll(func(e Event) {
secondCalled = true
})
bus.Publish(Event{Type: "test"})
if !secondCalled {
t.Fatal("second global handler was not called after first panicked")
}
}
func TestEventFieldsPassedThrough(t *testing.T) {
bus := New()
var received Event
bus.Subscribe("test:fields", func(e Event) {
received = e
})
bus.Publish(Event{
Type: "test:fields",
WorkspaceID: "ws-123",
ActorType: "member",
ActorID: "user-456",
Payload: map[string]string{"key": "value"},
})
if received.WorkspaceID != "ws-123" {
t.Errorf("expected WorkspaceID ws-123, got %s", received.WorkspaceID)
}
if received.ActorType != "member" {
t.Errorf("expected ActorType member, got %s", received.ActorType)
}
if received.ActorID != "user-456" {
t.Errorf("expected ActorID user-456, got %s", received.ActorID)
}
}