feat(desktop): push publish tasks via AMQP topic dispatch WebSocket
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>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user