diff --git a/apps/desktop/src/stores/__tests__/connectionStore.oneTimeCleanup.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.oneTimeCleanup.spec.ts index f8eebdcb7b..7ed4b0b98e 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.oneTimeCleanup.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.oneTimeCleanup.spec.ts @@ -18,9 +18,15 @@ function installApiMocks() { connectDb: vi.fn().mockResolvedValue("preview-1"), disconnectDb: vi.fn().mockResolvedValue(undefined), deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + loadConnections: vi.fn().mockResolvedValue([]), + loadEditorSettings: vi.fn().mockResolvedValue(null), + loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]), loadSchemaCache: vi.fn().mockResolvedValue(null), + loadTunnelProfiles: vi.fn().mockResolvedValue([]), saveConnections: vi.fn().mockResolvedValue(undefined), + saveEditorSettings: vi.fn().mockResolvedValue(undefined), saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + loadSidebarLayout: vi.fn().mockResolvedValue(null), connectionDatabaseInfo: vi.fn().mockResolvedValue(undefined), listInstalledAgents: vi.fn().mockResolvedValue([]), sessionCredentialStatus: vi.fn().mockResolvedValue(false), @@ -77,6 +83,36 @@ describe("connectionStore one_time runtime cleanup", () => { expect(disconnectDb).toHaveBeenCalledWith("preview-1"); }); + it("initFromDisk preserves an open one_time connection during a persisted-list reload", async () => { + installApiMocks(); + const { loadConnections } = await import("@/lib/backend/api"); + vi.mocked(loadConnections).mockResolvedValue([ + previewConnection({ id: "saved-1", name: "Saved", one_time: false }), + ]); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [previewConnection({ id: "deeplink-1", name: "Deeplink", one_time: true })]; + + await store.initFromDisk(); + + expect(store.connections.map((connection) => connection.id)).toEqual(["saved-1", "deeplink-1"]); + expect(store.getConfig("deeplink-1")).toMatchObject({ name: "Deeplink", one_time: true }); + }); + + it("does not retain a stale saved connection removed from persisted storage", async () => { + installApiMocks(); + const { loadConnections } = await import("@/lib/backend/api"); + vi.mocked(loadConnections).mockResolvedValue([]); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [previewConnection({ id: "removed-1", name: "Removed", one_time: false })]; + + await store.initFromDisk(); + + expect(store.getConfig("removed-1")).toBeUndefined(); + expect(store.connections).toEqual([]); + }); + it("removeConnection leaves saved connections to the save_connections sync", async () => { installApiMocks(); const { useConnectionStore } = await import("@/stores/connectionStore"); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 6070472d87..be1c07d17c 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -8035,8 +8035,14 @@ export const useConnectionStore = defineStore("connection", () => { const [pinnedOrder, saved] = await Promise.all([loadPinnedTreeNodeOrder(), api.loadConnections(), tunnelProfileStore.init()]); setPinnedTreeNodeOrder(pinnedOrder); await migrateTimeoutInheritance(saved); - connections.value = saved.map(normalizeConnection); - if (connections.value.some((connection, index) => (connection.connect_timeout_inherit === true && connection.connect_timeout_secs !== saved[index]?.connect_timeout_secs) || (connection.query_timeout_inherit === true && connection.query_timeout_secs !== saved[index]?.query_timeout_secs))) { + const loadedConnections = saved.map(normalizeConnection); + const loadedIds = new Set(loadedConnections.map((connection) => connection.id)); + // One-time deep-link connections are intentionally filtered from disk + // persistence. Keep them in the live list when an unrelated disk reload + // (for example, the scheduled-backup poll) completes while they are open. + const runtimeOneTimeConnections = connections.value.filter((connection) => connection.one_time === true && !loadedIds.has(connection.id)); + connections.value = [...loadedConnections, ...runtimeOneTimeConnections]; + if (loadedConnections.some((connection, index) => (connection.connect_timeout_inherit === true && connection.connect_timeout_secs !== saved[index]?.connect_timeout_secs) || (connection.query_timeout_inherit === true && connection.query_timeout_secs !== saved[index]?.query_timeout_secs))) { await persistConnections(); } syncTimeoutInheritanceBackup();