9fa091c150
Introduce a desktop.task.dispatch topic exchange with per-client routing keys. Tenant-api publishes a task_available frame keyed by target client ID when a publish job is created; every instance binds its own transient queue and forwards to the matching WebSocket (/api/desktop/dispatch) it owns. Desktop-client now prefers this push channel and only falls back to HTTP /lease polling when the socket is down. Also drop admin-web monitoring-plugin remnants and scope the publish modal account list to publish platforms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
3.5 KiB
Go
136 lines
3.5 KiB
Go
package stream
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
amqp "github.com/rabbitmq/amqp091-go"
|
|
)
|
|
|
|
func TestDispatchHub_PublishRoutesToLocalSubscriber(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"})
|
|
sub, cancel := hub.Subscribe("client-a")
|
|
defer cancel()
|
|
|
|
event := DesktopDispatchEvent{
|
|
Type: "task_available",
|
|
TaskID: "task-1",
|
|
TargetClientID: "client-a",
|
|
Kind: "publish",
|
|
Status: "queued",
|
|
}
|
|
hub.Publish(event)
|
|
|
|
select {
|
|
case got := <-sub.Outbound():
|
|
if got.TaskID != "task-1" {
|
|
t.Fatalf("unexpected task id: %s", got.TaskID)
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("expected to receive event within 200ms")
|
|
}
|
|
}
|
|
|
|
func TestDispatchHub_DropsEventsForUnknownClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"})
|
|
|
|
// Subscribe for a different client, publish for another one.
|
|
_, cancel := hub.Subscribe("client-b")
|
|
defer cancel()
|
|
|
|
hub.Publish(DesktopDispatchEvent{
|
|
Type: "task_available",
|
|
TargetClientID: "client-a",
|
|
})
|
|
|
|
ids := hub.ActiveClientIDs()
|
|
if len(ids) != 1 || ids[0] != "client-b" {
|
|
t.Fatalf("expected only client-b subscribed, got %v", ids)
|
|
}
|
|
}
|
|
|
|
func TestDispatchHub_CancelRemovesSubscriber(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"})
|
|
sub, cancel := hub.Subscribe("client-a")
|
|
|
|
cancel()
|
|
|
|
// Subsequent publish must not panic even though the subscriber is gone;
|
|
// channel is closed, reads must yield zero value with !ok.
|
|
hub.Publish(DesktopDispatchEvent{TargetClientID: "client-a"})
|
|
|
|
select {
|
|
case _, ok := <-sub.Outbound():
|
|
if ok {
|
|
t.Fatal("expected outbound channel to be closed after cancel")
|
|
}
|
|
case <-time.After(50 * time.Millisecond):
|
|
t.Fatal("expected closed channel to yield immediately")
|
|
}
|
|
|
|
if ids := hub.ActiveClientIDs(); len(ids) != 0 {
|
|
t.Fatalf("expected no subscribers after cancel, got %v", ids)
|
|
}
|
|
}
|
|
|
|
func TestDispatchHub_MultipleSubscribersSameClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"})
|
|
subA, cancelA := hub.Subscribe("client-a")
|
|
defer cancelA()
|
|
subB, cancelB := hub.Subscribe("client-a")
|
|
defer cancelB()
|
|
|
|
hub.Publish(DesktopDispatchEvent{
|
|
Type: "task_available",
|
|
TaskID: "task-1",
|
|
TargetClientID: "client-a",
|
|
})
|
|
|
|
for i, ch := range []<-chan DesktopDispatchEvent{subA.Outbound(), subB.Outbound()} {
|
|
select {
|
|
case got := <-ch:
|
|
if got.TaskID != "task-1" {
|
|
t.Fatalf("subscriber %d got unexpected task id: %s", i, got.TaskID)
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatalf("subscriber %d did not receive event", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDispatchHub_HandleDeliveryFallsBackToRoutingKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"})
|
|
sub, cancel := hub.Subscribe("client-c")
|
|
defer cancel()
|
|
|
|
// Body intentionally omits target_client_id; the hub should recover it
|
|
// from the routing key "<prefix>.<client_id>".
|
|
delivery := amqp.Delivery{
|
|
RoutingKey: "publish.client-c",
|
|
Body: []byte(`{"type":"task_available","task_id":"task-9","kind":"publish","status":"queued"}`),
|
|
}
|
|
hub.handleDelivery(delivery)
|
|
|
|
select {
|
|
case got := <-sub.Outbound():
|
|
if got.TargetClientID != "client-c" {
|
|
t.Fatalf("expected fallback to set target client id, got %q", got.TargetClientID)
|
|
}
|
|
if got.TaskID != "task-9" {
|
|
t.Fatalf("unexpected task id: %s", got.TaskID)
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("expected fallback-routed event")
|
|
}
|
|
}
|