This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/events/bus_test.go
2026-03-19 18:50:27 +00:00

215 lines
5.7 KiB
Go

package events
import (
"encoding/json"
"sync/atomic"
"testing"
"time"
)
func TestMatch(t *testing.T) {
tests := []struct {
label, pattern string
want bool
}{
{"chat.message.abc", "chat.message.abc", true},
{"chat.message.abc", "chat.message.*", true},
{"chat.message.abc", "chat.*", true},
{"chat.message.abc", "*", true},
{"chat.message.abc", "chat.message.xyz", false},
{"chat.message.abc", "channel.message.*", false},
{"chat.message.abc", "chat.message", false},
{"ping", "ping", true},
{"ping", "pong", false},
{"plugin.hook.pre_completion", "plugin.hook.*", true},
{"plugin.hook.pre_completion", "plugin.*", true},
}
for _, tt := range tests {
got := match(tt.label, tt.pattern)
if got != tt.want {
t.Errorf("match(%q, %q) = %v, want %v", tt.label, tt.pattern, got, tt.want)
}
}
}
func TestBusPublishExact(t *testing.T) {
bus := NewBus()
var count int32
bus.Subscribe("chat.message.abc", func(e Event) {
atomic.AddInt32(&count, 1)
})
bus.Publish(Event{Label: "chat.message.abc", Payload: json.RawMessage(`{}`)})
bus.Publish(Event{Label: "chat.message.xyz", Payload: json.RawMessage(`{}`)})
if atomic.LoadInt32(&count) != 1 {
t.Errorf("expected 1 dispatch, got %d", count)
}
}
func TestBusPublishWildcard(t *testing.T) {
bus := NewBus()
var count int32
bus.Subscribe("chat.message.*", func(e Event) {
atomic.AddInt32(&count, 1)
})
bus.Publish(Event{Label: "chat.message.abc", Payload: json.RawMessage(`{}`)})
bus.Publish(Event{Label: "chat.message.xyz", Payload: json.RawMessage(`{}`)})
bus.Publish(Event{Label: "chat.typing.abc", Payload: json.RawMessage(`{}`)})
if atomic.LoadInt32(&count) != 2 {
t.Errorf("expected 2 dispatches, got %d", count)
}
}
func TestBusUnsubscribe(t *testing.T) {
bus := NewBus()
var count int32
unsub := bus.Subscribe("test.event", func(e Event) {
atomic.AddInt32(&count, 1)
})
bus.Publish(Event{Label: "test.event", Payload: json.RawMessage(`{}`)})
unsub()
bus.Publish(Event{Label: "test.event", Payload: json.RawMessage(`{}`)})
if atomic.LoadInt32(&count) != 1 {
t.Errorf("expected 1 dispatch after unsub, got %d", count)
}
}
func TestBusGlobalWildcard(t *testing.T) {
bus := NewBus()
var count int32
bus.Subscribe("*", func(e Event) {
atomic.AddInt32(&count, 1)
})
bus.Publish(Event{Label: "chat.message.abc", Payload: json.RawMessage(`{}`)})
bus.Publish(Event{Label: "system.notify", Payload: json.RawMessage(`{}`)})
bus.Publish(Event{Label: "plugin.hook.pre_completion", Payload: json.RawMessage(`{}`)})
if atomic.LoadInt32(&count) != 3 {
t.Errorf("expected 3 dispatches, got %d", count)
}
}
func TestRouteFor(t *testing.T) {
tests := []struct {
label string
want Direction
}{
{"chat.message.abc", DirBoth},
{"chat.typing.abc", DirBoth},
{"system.notify", DirToClient},
{"plugin.hook.pre_completion", DirLocal},
{"internal.db.write", DirLocal},
{"unknown.event", DirLocal}, // default
{"ping", DirFromClient},
{"pong", DirToClient},
// Tool bridge routes
{"tool.call.abc123", DirToClient},
{"tool.result.abc123", DirBoth}, // v0.32.0: DirBoth for cross-pod WaitFor
// Extension lifecycle
{"extension.loaded", DirLocal},
{"extension.error", DirLocal},
}
for _, tt := range tests {
got := RouteFor(tt.label)
if got != tt.want {
t.Errorf("RouteFor(%q) = %d, want %d", tt.label, got, tt.want)
}
}
}
func TestWaitFor_Success(t *testing.T) {
bus := NewBus()
// Publish after a short delay
go func() {
time.Sleep(10 * time.Millisecond)
bus.Publish(Event{
Label: "tool.result.abc123",
Payload: json.RawMessage(`{"result":"hello"}`),
})
}()
event, ok := bus.WaitFor("tool.result.abc123", 1*time.Second)
if !ok {
t.Fatal("WaitFor timed out, expected success")
}
if event.Label != "tool.result.abc123" {
t.Errorf("expected label tool.result.abc123, got %s", event.Label)
}
if string(event.Payload) != `{"result":"hello"}` {
t.Errorf("unexpected payload: %s", string(event.Payload))
}
}
func TestWaitFor_Timeout(t *testing.T) {
bus := NewBus()
_, ok := bus.WaitFor("tool.result.never", 50*time.Millisecond)
if ok {
t.Error("WaitFor should have timed out")
}
}
func TestWaitFor_IgnoresOtherLabels(t *testing.T) {
bus := NewBus()
go func() {
time.Sleep(5 * time.Millisecond)
bus.Publish(Event{Label: "tool.result.other", Payload: json.RawMessage(`{}`)})
time.Sleep(5 * time.Millisecond)
bus.Publish(Event{Label: "tool.result.target", Payload: json.RawMessage(`{"ok":true}`)})
}()
event, ok := bus.WaitFor("tool.result.target", 1*time.Second)
if !ok {
t.Fatal("WaitFor timed out")
}
if event.Label != "tool.result.target" {
t.Errorf("got wrong label: %s", event.Label)
}
}
func TestWaitFor_CleansUpSubscription(t *testing.T) {
bus := NewBus()
// WaitFor with immediate timeout
bus.WaitFor("tool.result.cleanup", 1*time.Millisecond)
time.Sleep(5 * time.Millisecond)
// Verify no panic when publishing to the label after cleanup
bus.Publish(Event{Label: "tool.result.cleanup", Payload: json.RawMessage(`{}`)})
}
func TestToolCallRouteToClient(t *testing.T) {
if !ShouldSendToClient("tool.call.abc123") {
t.Error("tool.call.* should be sent to client")
}
if ShouldAcceptFromClient("tool.call.abc123") {
t.Error("tool.call.* should NOT be accepted from client")
}
}
func TestToolResultRouteBoth(t *testing.T) {
// v0.32.0: tool.result is DirBoth so results cross pods for WaitFor.
// The WS subscriber explicitly filters out tool.result events to
// prevent re-sending to clients (see subscribeToBus in ws.go).
if !ShouldAcceptFromClient("tool.result.abc123") {
t.Error("tool.result.* should be accepted from client")
}
if !ShouldSendToClient("tool.result.abc123") {
t.Error("tool.result.* should route DirBoth (filtered by WS subscriber, not routing table)")
}
}