From 692445efa955e2de653b86a1bfdd37fcaf3dc542 Mon Sep 17 00:00:00 2001 From: Delicious233 <101502465+DeliciousBuding@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:21:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?test(hub):=20=E8=A6=86=E7=9B=96=20target-bo?= =?UTF-8?q?und=20dispatch=20=E4=B8=8D=E5=9B=9E=E9=80=80=E7=BA=A2=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dispatchTargetBoundTask 此前零测试覆盖。新增两条用例:route 不可用时与 conn 不匹配时均推 target 离线队列(PushPendingTargetTask),不推 inviter desktop 队列(PushPendingTask)、不推 WS conn。锁定架构红线"target-bound 离线不静默回退到 inviter desktop"。 Co-authored-by: Cursor --- .../agent_dispatch_target_bound_test.go | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go diff --git a/hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go b/hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go new file mode 100644 index 000000000..190b0a0d4 --- /dev/null +++ b/hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go @@ -0,0 +1,83 @@ +package dispatchsvc + +import ( + "context" + "testing" + + "github.com/agenthub/hub-server/internal/config" + "github.com/agenthub/hub-server/internal/model" + "github.com/stretchr/testify/assert" +) + +// TestDispatchTargetBound_RouteUnavailable_QueuesTargetTask verifies the +// architecture red line: when the route for the target device is unavailable +// (offline / no WS connection), the task is pushed to the TARGET's offline +// queue (PushPendingTargetTask), NOT the inviter desktop queue +// (PushPendingTask). See docs/architecture/01-hub-server.md §Runtime And +// Team Routing: "must not silently fallback to another Desktop/Edge when a +// target-bound device is offline." +func TestDispatchTargetBound_RouteUnavailable_QueuesTargetTask(t *testing.T) { + cache := &recordingDispatchCache{ + routes: map[string]string{}, // no route for this device → empty connID + } + ws := &recordingDispatchWS{conn: nil} // no active conn + + ds := NewDispatchService( + nil, // db — not reached on this failure path + nil, // bus + ws, // mgr + cache, // cacheClient + nil, // relay + nil, // outbox + config.EdgeDispatchConfig{}, + nil, // edgeClient + "", // jwtSecret + ) + + task := &model.PendingAgentTask{ + ID: "task-target-1", + TargetID: "target-desktop-1", + } + userID := "user-1" + deviceID := "device-1" + payload := []byte(`{"task_id":"task-target-1"}`) + + delivered := ds.dispatchTargetBoundTask(context.Background(), cache, task, userID, deviceID, payload) + + assert.False(t, delivered, "target-bound task with unavailable route must not be delivered") + assert.Len(t, cache.pushed, 1, "must push to target queue exactly once") + assert.Contains(t, cache.pushed[0], userID, "push must include userID") + assert.Contains(t, cache.pushed[0], "target-desktop-1", "push must include targetID") + assert.Contains(t, cache.pushed[0], deviceID, "push must include deviceID") + assert.Equal(t, 0, ws.pushed, "must NOT push to WS conn (target is offline)") +} + +// TestDispatchTargetBound_ConnMismatch_QueuesTargetTask verifies that when +// the device route returns a connID but no matching connection is found +// (stale route / conn dropped between lookup and dispatch), the task still +// goes to the target queue, not the inviter desktop queue. +func TestDispatchTargetBound_ConnMismatch_QueuesTargetTask(t *testing.T) { + cache := &recordingDispatchCache{ + routes: map[string]string{ + "user-1:desktop:device-1": "conn-stale-1", // route exists but conn is gone + }, + } + ws := &recordingDispatchWS{conn: nil} // FindByConnID returns nil + + ds := NewDispatchService( + nil, nil, ws, cache, nil, nil, config.EdgeDispatchConfig{}, nil, "", + ) + + task := &model.PendingAgentTask{ + ID: "task-target-2", + TargetID: "target-desktop-2", + } + payload := []byte(`{"task_id":"task-target-2"}`) + + delivered := ds.dispatchTargetBoundTask(context.Background(), cache, task, "user-1", "device-1", payload) + + assert.False(t, delivered, "target-bound task with stale conn must not be delivered") + assert.Len(t, cache.pushed, 1, "must push to target queue exactly once") + assert.Contains(t, cache.pushed[0], "target-desktop-2", "push must include targetID") + assert.Equal(t, 0, ws.pushed, "must NOT push to WS conn (conn not found)") +} From 342479a8b0ba83e5c947c627854c18ab12b7d6bd Mon Sep 17 00:00:00 2001 From: Delicious233 <101502465+DeliciousBuding@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:02:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(hub):=20=E6=96=AD=E8=A8=80=20target-bo?= =?UTF-8?q?und=20=E9=98=9F=E5=88=97=E6=8E=A5=E6=94=B6=E5=8E=9F=E5=A7=8B=20?= =?UTF-8?q?task=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../service/dispatchsvc/agent_dispatch_target_bound_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go b/hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go index 190b0a0d4..d5681e197 100644 --- a/hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go +++ b/hub-server/internal/service/dispatchsvc/agent_dispatch_target_bound_test.go @@ -49,6 +49,7 @@ func TestDispatchTargetBound_RouteUnavailable_QueuesTargetTask(t *testing.T) { assert.Contains(t, cache.pushed[0], userID, "push must include userID") assert.Contains(t, cache.pushed[0], "target-desktop-1", "push must include targetID") assert.Contains(t, cache.pushed[0], deviceID, "push must include deviceID") + assert.Contains(t, cache.pushed[0], string(payload), "push must include the original task JSON, not an altered copy") assert.Equal(t, 0, ws.pushed, "must NOT push to WS conn (target is offline)") } @@ -79,5 +80,6 @@ func TestDispatchTargetBound_ConnMismatch_QueuesTargetTask(t *testing.T) { assert.False(t, delivered, "target-bound task with stale conn must not be delivered") assert.Len(t, cache.pushed, 1, "must push to target queue exactly once") assert.Contains(t, cache.pushed[0], "target-desktop-2", "push must include targetID") + assert.Contains(t, cache.pushed[0], string(payload), "push must include the original task JSON, not an altered copy") assert.Equal(t, 0, ws.pushed, "must NOT push to WS conn (conn not found)") }