- Add internal event bus (server/internal/events/) with synchronous pub/sub and panic isolation per listener - Upgrade WebSocket Hub to workspace-scoped rooms with JWT auth and membership verification on connect - Add 10 new WS event types (comment CRUD, inbox read/archive, agent create/delete, workspace/member events) - Refactor all handlers and TaskService to publish events via Bus instead of direct Hub.Broadcast calls - Add WS broadcast listener that routes events to correct workspace - Frontend: WSClient sends token + workspace_id on connect with auto-reconnect refetch - Frontend: centralized useRealtimeSync hook dispatches all WS events to global Zustand stores - Migrate issues and inbox pages from local useState to global useIssueStore/useInboxStore - Make store addIssue/addItem idempotent to prevent duplicates - Remove dead packages/hooks/src/use-realtime.ts - Add feature tracking files for 4 planned features Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
1.8 KiB
Go
90 lines
1.8 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 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)
|
|
}
|
|
}
|