From 5b0819d751862a1baa4bbc9ba2806ef229acefb1 Mon Sep 17 00:00:00 2001 From: "Codex (Delicious233)" Date: Sun, 6 Sep 2026 02:31:13 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(desktop):=20=E5=88=B7=E6=96=B0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E6=B3=A8=E5=86=8C=E5=90=8E=E7=9A=84=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E7=9B=AE=E6=A0=87=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DesktopHubTaskBridge.test.tsx | 92 +++++++++++++++++++ .../src/components/DesktopHubTaskBridge.tsx | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 app/desktop/src/components/DesktopHubTaskBridge.test.tsx diff --git a/app/desktop/src/components/DesktopHubTaskBridge.test.tsx b/app/desktop/src/components/DesktopHubTaskBridge.test.tsx new file mode 100644 index 000000000..05a0e3108 --- /dev/null +++ b/app/desktop/src/components/DesktopHubTaskBridge.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { cleanup, render, renderHook, waitFor } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { hubQueryKeys } from '@shared/stores/queryKeys'; +import { queryClient } from '@/api/queryClient'; +import { useHubExecutionTargets } from '@/api/executionTargetQueries'; +import DesktopHubTaskBridge from './DesktopHubTaskBridge'; + +const fixture = vi.hoisted(() => ({ + registration: { status: 'registered', deviceId: 'fixture-device' }, + listExecutionTargets: vi.fn(), + auth: { isAuthenticated: true, token: 'fixture-token', tryAutoLogin: vi.fn() }, +})); + +vi.mock('@/api/hubClient', () => ({ + createHubClient: () => ({ listExecutionTargets: fixture.listExecutionTargets }), +})); +vi.mock('@/hooks/useAuth', () => ({ + getAccessToken: () => fixture.auth.token, + useAuth: () => fixture.auth, +})); +vi.mock('@/hooks/useDeviceRegistration', () => ({ + useDeviceRegistration: () => fixture.registration, +})); +vi.mock('@/hooks/useHubEventStream', () => ({ + useHubEventStream: () => ({ status: 'disconnected', hubWS: null }), +})); +vi.mock('@/hooks/useHubIntegration', () => ({ useHubIntegration: vi.fn() })); +vi.mock('@/hooks/useHealth', () => ({ useHealth: () => ({ online: false }) })); +vi.mock('@/api/agentQueries', () => ({ useAgentList: () => ({ data: undefined }) })); +vi.mock('@/api/modelCatalogQueries', () => ({ useModelCatalog: () => ({ data: undefined }) })); +vi.mock('@/config', () => ({ getEdgeBaseUrl: () => 'http://edge.test' })); + +function Wrapper({ children }: React.PropsWithChildren) { + return {children}; +} + +async function seedLiveTargets() { + // The production hook supplies the cache key; do not prove a made-up key + // invalidates itself. A fresh entry also prevents mount-refetch hiding a miss. + const hook = renderHook(() => useHubExecutionTargets({ enabled: true }), { wrapper: Wrapper }); + await waitFor(() => expect(hook.result.current.isSuccess).toBe(true)); + const entry = queryClient.getQueryCache().getAll()[0]; + if (!entry) throw new Error('The production target hook registered no cache entry'); + hook.unmount(); + expect(fixture.listExecutionTargets).toHaveBeenCalledTimes(1); + return entry.queryKey; +} + +beforeEach(() => { + queryClient.clear(); + queryClient.setDefaultOptions({ queries: { retry: false, staleTime: Infinity } }); + vi.clearAllMocks(); + fixture.registration.status = 'registered'; + fixture.listExecutionTargets.mockResolvedValue({ items: [], page: { hasMore: false } }); +}); + +afterEach(() => { + cleanup(); + queryClient.clear(); +}); + +describe('DesktopHubTaskBridge target refresh', () => { + it('refetches the live target cache after registration without invalidating unrelated Hub data', async () => { + const targetKey = await seedLiveTargets(); + queryClient.setQueryData(hubQueryKeys.contacts.list, []); + fixture.listExecutionTargets.mockResolvedValue({ + items: [{ id: 'new-target', name: 'New target', target_type: 'local_edge', health_state: 'offline' }], + page: { hasMore: false }, + }); + + render(, { wrapper: Wrapper }); + + // This must happen before the hook's 10-second fallback poll. + await waitFor(() => expect(queryClient.getQueryData(targetKey)).toMatchObject({ + items: [{ id: 'new-target' }], + })); + expect(fixture.listExecutionTargets).toHaveBeenCalledTimes(2); + expect(queryClient.getQueryState(hubQueryKeys.contacts.list)?.isInvalidated).toBe(false); + }); + + it('does not invalidate or refetch targets while registration is pending', async () => { + const targetKey = await seedLiveTargets(); + fixture.registration.status = 'registering'; + + render(, { wrapper: Wrapper }); + + expect(queryClient.getQueryState(targetKey)?.isInvalidated).toBe(false); + expect(fixture.listExecutionTargets).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/desktop/src/components/DesktopHubTaskBridge.tsx b/app/desktop/src/components/DesktopHubTaskBridge.tsx index 85bd563ce..85765cacc 100644 --- a/app/desktop/src/components/DesktopHubTaskBridge.tsx +++ b/app/desktop/src/components/DesktopHubTaskBridge.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef } from 'react'; import { queryClient } from '@/api/queryClient'; +import { hubQueryKeys } from '@shared/stores/queryKeys'; import { getAccessToken, useAuth } from '@/hooks/useAuth'; import { createHubClient } from '@/api/hubClient'; import { @@ -83,7 +84,7 @@ function DesktopHubTaskBridgeActive() { useEffect(() => { if (!deviceReady) return; - void queryClient.invalidateQueries({ queryKey: ['execution-targets'] }); + void queryClient.invalidateQueries({ queryKey: hubQueryKeys.executionTargets.root }); }, [deviceReady, deviceRegistration.deviceId]); // Mirror live connection health into the connection store so the shell can From 3434ceac132216e35c8993d211739db2c0f9285d Mon Sep 17 00:00:00 2001 From: "Codex (Delicious233)" Date: Sun, 6 Sep 2026 02:47:38 +0800 Subject: [PATCH 2/2] test(desktop): replace stale target invalidation assertion --- app/desktop/src/__tests__/App.v4.test.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/desktop/src/__tests__/App.v4.test.tsx b/app/desktop/src/__tests__/App.v4.test.tsx index dfb8dc186..273773e29 100644 --- a/app/desktop/src/__tests__/App.v4.test.tsx +++ b/app/desktop/src/__tests__/App.v4.test.tsx @@ -572,9 +572,6 @@ describe('Desktop App v4 root', () => { }); }); expect(mockedUseDeviceRegistration).toHaveBeenCalledWith(mockHubClient); - await waitFor(() => { - expect(mockedQueryClient.invalidateQueries).toHaveBeenCalledWith({ queryKey: ['execution-targets'] }); - }); expect(mockedUseHubEventStream).toHaveBeenCalledWith(getAccessToken); expect(mockedCreateHubClient).toHaveBeenCalledWith({ getToken: getAccessToken }); expect(tryAutoLogin).not.toHaveBeenCalled();