Files
kandev/apps/backend/internal/gateway/websocket/agent_runtime_notifications_test.go
T
2026-09-06 16:09:03 +00:00

125 lines
4.2 KiB
Go

package websocket
import (
"context"
"encoding/json"
"sync"
"testing"
"time"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/events/bus"
storagepkg "github.com/kandev/kandev/internal/system/storage"
ws "github.com/kandev/kandev/pkg/websocket"
)
func TestAgentRuntimeNotificationsBroadcastAndReplaySanitizedState(t *testing.T) {
hub := newTestHub(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
go hub.Run(ctx)
eventBus := bus.NewMemoryEventBus(testLoggerForUserNotifications(t))
t.Cleanup(eventBus.Close)
payload := map[string]any{"status": "unavailable", "reason": "agentctl_exited"}
broadcaster := RegisterAgentRuntimeNotifications(ctx, eventBus, hub, func() (any, bool) {
return payload, true
}, testLoggerForUserNotifications(t))
t.Cleanup(broadcaster.Close)
liveClient := newTestClient("live")
registerTestClient(hub, liveClient)
if err := eventBus.Publish(ctx, events.AgentRuntimeAvailabilityChanged,
bus.NewEvent(events.AgentRuntimeAvailabilityChanged, "test", payload)); err != nil {
t.Fatalf("publish availability event: %v", err)
}
liveMessage := readNotification(t, liveClient)
if liveMessage.Action != ws.ActionSystemAgentRuntimeStatusChanged {
t.Fatalf("live action = %q, want %q", liveMessage.Action, ws.ActionSystemAgentRuntimeStatusChanged)
}
replayClient := newTestClient("replay")
registerTestClient(hub, replayClient)
hub.SubscribeToUser(replayClient, "user-1")
replayMessage := readNotification(t, replayClient)
if replayMessage.Action != ws.ActionSystemAgentRuntimeStatusChanged {
t.Fatalf("replay action = %q, want %q", replayMessage.Action, ws.ActionSystemAgentRuntimeStatusChanged)
}
var replayPayload map[string]any
if err := json.Unmarshal(replayMessage.Payload, &replayPayload); err != nil {
t.Fatalf("decode replay payload: %v", err)
}
if replayPayload["status"] != "unavailable" || replayPayload["reason"] != "agentctl_exited" {
t.Fatalf("replay payload = %#v", replayPayload)
}
}
func TestSystemEventBroadcasterCloseIsIdempotent(t *testing.T) {
log := testLoggerForUserNotifications(t)
eventBus := bus.NewMemoryEventBus(log)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
hub := newTestHub(t)
broadcaster := RegisterSystemNotifications(ctx, eventBus, hub, log)
var wg sync.WaitGroup
for index := 0; index < 16; index++ {
wg.Add(1)
go func() {
defer wg.Done()
broadcaster.Close()
}()
}
wg.Wait()
if len(broadcaster.subscriptions) != 0 {
t.Fatalf("subscriptions after concurrent Close = %d, want 0", len(broadcaster.subscriptions))
}
eventBus.Close()
}
func TestSystemStorageAnalysisNotificationsBroadcastProgressState(t *testing.T) {
hub := newTestHub(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
go hub.Run(ctx)
eventBus := bus.NewMemoryEventBus(testLoggerForUserNotifications(t))
t.Cleanup(eventBus.Close)
broadcaster := RegisterSystemNotifications(ctx, eventBus, hub, testLoggerForUserNotifications(t))
t.Cleanup(broadcaster.Close)
client := newTestClient("storage")
registerTestClient(hub, client)
payload := storagepkg.StorageAnalysisUpdated{Generation: 3, State: storagepkg.AnalysisStateScanning}
if err := eventBus.Publish(ctx, events.SystemStorageAnalysisUpdated,
bus.NewEvent(events.SystemStorageAnalysisUpdated, "test", payload)); err != nil {
t.Fatalf("publish storage event: %v", err)
}
message := readNotification(t, client)
if message.Action != ws.ActionSystemStorageAnalysisUpdated {
t.Fatalf("storage action = %q, want %q", message.Action, ws.ActionSystemStorageAnalysisUpdated)
}
var received storagepkg.StorageAnalysisUpdated
if err := json.Unmarshal(message.Payload, &received); err != nil {
t.Fatalf("decode storage payload: %v", err)
}
if received != payload {
t.Fatalf("storage payload = %#v, want %#v", received, payload)
}
}
func readNotification(t *testing.T, client *Client) ws.Message {
t.Helper()
select {
case frame := <-client.send:
var message ws.Message
if err := json.Unmarshal(frame, &message); err != nil {
t.Fatalf("decode websocket frame: %v", err)
}
return message
case <-time.After(time.Second):
t.Fatal("timed out waiting for websocket notification")
return ws.Message{}
}
}