From 3c431299a3c12bdda4558ee00fbb731aa2455643 Mon Sep 17 00:00:00 2001 From: m4air Date: Thu, 8 Oct 2026 12:43:03 -0700 Subject: [PATCH] Keep open Remote Settings subscribed to the saved-server catalog --- .../use-runtime-environment-catalog.test.tsx | 79 +++++++++++++++++++ .../use-runtime-environment-catalog.ts | 8 +- 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx diff --git a/src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx b/src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx new file mode 100644 index 000000000000..3259773ab71e --- /dev/null +++ b/src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx @@ -0,0 +1,79 @@ +// @vitest-environment happy-dom + +import { act, cleanup, renderHook, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments' +import { useAppStore } from '@/store' +import { useRuntimeEnvironmentCatalog } from './use-runtime-environment-catalog' + +const initialState = useAppStore.getInitialState() +const list = vi.fn<() => Promise>() +const getStatus = vi.fn(async () => { + throw new Error('host is unreachable') +}) +const previousApi = Object.getOwnPropertyDescriptor(window, 'api') +const environment: PublicKnownRuntimeEnvironment = { + id: 'env-a', + name: 'Managed host', + createdAt: 1, + updatedAt: 1, + pairingRevision: 1, + lastUsedAt: null, + runtimeId: null, + endpoints: [ + { id: 'ws-a', kind: 'websocket', label: 'WebSocket', endpoint: 'ws://localhost:6768' } + ], + preferredEndpointId: 'ws-a' +} + +beforeEach(() => { + useAppStore.setState(initialState, true) + list.mockReset().mockResolvedValue([]) + getStatus.mockClear() + Object.defineProperty(window, 'api', { + configurable: true, + value: { + runtimeEnvironments: { list, getStatus, getStatusSnapshots: async () => [] } + } + }) +}) +afterEach(() => { + cleanup() + useAppStore.setState(initialState, true) + if (previousApi) { + Object.defineProperty(window, 'api', previousApi) + } else { + Reflect.deleteProperty(window, 'api') + } +}) + +it('shows a managed host published after Settings finished its initial list request', async () => { + const { result } = renderHook(useRuntimeEnvironmentCatalog) + await waitFor(() => expect(result.current.isLoading).toBe(false)) + expect(list).toHaveBeenCalledTimes(1) + act(() => useAppStore.setState({ runtimeEnvironments: [environment] })) + expect(result.current.environments).toEqual([environment]) + expect(list).toHaveBeenCalledTimes(1) + expect(getStatus).not.toHaveBeenCalled() +}) + +it('follows saved host changes while contact with the host is unavailable', async () => { + list.mockResolvedValue([environment]) + const { result } = renderHook(useRuntimeEnvironmentCatalog) + await waitFor(() => expect(result.current.isLoading).toBe(false)) + const renamed = { ...environment, name: 'Renamed host', updatedAt: 2 } + act(() => useAppStore.setState({ runtimeEnvironments: [renamed] })) + expect(result.current.environments).toEqual([renamed]) + act(() => useAppStore.setState({ runtimeEnvironments: [] })) + expect(result.current.environments).toEqual([]) + expect(list).toHaveBeenCalledTimes(1) + expect(getStatus).toHaveBeenCalledTimes(1) +}) + +it('continues hiding recipe-created VM environments from the saved-server list', async () => { + const { result } = renderHook(useRuntimeEnvironmentCatalog) + await waitFor(() => expect(result.current.isLoading).toBe(false)) + const vm: PublicKnownRuntimeEnvironment = { ...environment, id: 'env-vm', source: 'ephemeral-vm' } + act(() => useAppStore.setState({ runtimeEnvironments: [vm, environment] })) + expect(result.current.environments).toEqual([environment]) +}) diff --git a/src/renderer/src/components/settings/use-runtime-environment-catalog.ts b/src/renderer/src/components/settings/use-runtime-environment-catalog.ts index 5470bf5b6e48..6db8a8f7e0c9 100644 --- a/src/renderer/src/components/settings/use-runtime-environment-catalog.ts +++ b/src/renderer/src/components/settings/use-runtime-environment-catalog.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, + useMemo, useState, type Dispatch, type MutableRefObject, @@ -32,7 +33,11 @@ type RuntimeEnvironmentCatalog = { } export function useRuntimeEnvironmentCatalog(): RuntimeEnvironmentCatalog { - const [environments, setEnvironments] = useState([]) + const savedEnvironments = useAppStore((state) => state.runtimeEnvironments) + const environments = useMemo( + () => savedEnvironments.filter(isUserManagedRuntimeEnvironment), + [savedEnvironments] + ) const [isLoading, setIsLoading] = useState(false) const [detailsByEnvironmentId, setDetailsByEnvironmentId] = useState< Record @@ -54,7 +59,6 @@ export function useRuntimeEnvironmentCatalog(): RuntimeEnvironmentCatalog { await useAppStore.getState().readRuntimeHostStatusSnapshots() } if (mountedRef.current) { - setEnvironments(visibleEnvironments) setDetailsByEnvironmentId((current) => { const next: Record = {} for (const environment of visibleEnvironments) {