From 5017aa7bb9974c14cfa6a6e342c0e8a41afc3374 Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 30 Sep 2026 19:46:49 +0800 Subject: [PATCH] fix(updates): sync driver store state after update center runs --- apps/desktop/src/App.vue | 3 +- .../components/config/DriverStoreDialog.vue | 8 +- ...iverStoreDialog.externalUpdateSync.spec.ts | 147 ++++++++++++++++++ .../__tests__/componentUpdateEvents.spec.ts | 13 +- .../src/lib/updates/componentUpdateEvents.ts | 6 + 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/components/config/__tests__/DriverStoreDialog.externalUpdateSync.spec.ts diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 94440bb1df..ed10d13a83 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -35,7 +35,7 @@ import { canDownloadAndInstallUpdate, useAppUpdater } from "@/composables/useApp import { useMcpUpdateBadge } from "@/composables/useMcpUpdateBadge"; import { useComponentUpdates, type ComponentUpdateCategory } from "@/composables/useComponentUpdates"; import type { PluginUpdateBlock } from "@/composables/useComponentUpdates"; -import { COMPONENT_UPDATES_CHANGED_EVENT, notifyComponentPluginsUpdated, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; +import { COMPONENT_UPDATES_CHANGED_EVENT, notifyComponentDriverUpdatesChanged, notifyComponentPluginsUpdated, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; import { driverStoreUpdateBadgeCount, showMcpUpdateBadge, showToolbarUpdateAction } from "@/lib/updates/updateBadges"; import { continuePreparedAppUpdate, @@ -1341,6 +1341,7 @@ function reportComponentUpdateResult(result: Awaited 0) notifyComponentPluginsUpdated(); + if (result.drivers > 0 || result.jdbc) notifyComponentDriverUpdatesChanged(); if (updatedComponents.length) toast(t("updates.componentsAutoUpdated", { components: updatedComponents.join(t("updates.componentListSeparator")) })); if (result.blockedDrivers.length) { toast(t("driverStore.driverUpdateBlocked", { labels: updateBlockerLabels(result.blockedDrivers).join(", ") }), 8000); diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 89a5f63084..d89f3b226a 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -40,7 +40,7 @@ import { translateBackendError } from "@/i18n/backend-errors"; import { runAgentOfflineExportAction } from "@/lib/driverStore/agentOfflineExportFlow"; import { DRIVER_CATEGORIES, getCategoryForAgentDriver, assertAgentDriverCategoriesComplete } from "@/lib/connection/driver-category-definitions"; import { hasAnyUpdatableDriverMatching, countInstalledDrivers, countAvailableDrivers, partitionDriversByInstallStatus, upgradeAllDriverTypes, upgradeAllMatchesFullUpdateSet, type DriverInstallStatusFilter } from "@/lib/connection/driverListFilter"; -import { notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; +import { COMPONENT_DRIVER_UPDATES_CHANGED_EVENT, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; import { updateBlockerLabels } from "@/lib/updates/componentUpdateOrchestration"; const { t } = useI18n(); @@ -431,6 +431,10 @@ async function forceRefresh() { } } +function handleComponentDriverUpdatesChanged() { + void Promise.allSettled([forceRefresh(), loadJdbcDrivers(), loadJdbcPluginStatus()]); +} + function setUpdateDownloadSource(value: unknown) { if (value !== "official" && value !== "cnb") return; if (value === settingsStore.editorSettings.updateDownloadSource) return; @@ -1460,6 +1464,7 @@ async function deleteJdbcLocalBundle(bundleId: string) { // ──────────── Lifecycle ──────────── onMounted(async () => { + window.addEventListener(COMPONENT_DRIVER_UPDATES_CHANGED_EVENT, handleComponentDriverUpdatesChanged); updateAgentDrivers(await api.listInstalledAgentsLocal()); void loadJavaRuntimeConfig(); void loadDriverStoreUsage(); @@ -1507,6 +1512,7 @@ onMounted(async () => { }); onUnmounted(() => { + window.removeEventListener(COMPONENT_DRIVER_UPDATES_CHANGED_EVENT, handleComponentDriverUpdatesChanged); unlisten?.(); stopDriverRuntimePolling(); }); diff --git a/apps/desktop/src/components/config/__tests__/DriverStoreDialog.externalUpdateSync.spec.ts b/apps/desktop/src/components/config/__tests__/DriverStoreDialog.externalUpdateSync.spec.ts new file mode 100644 index 0000000000..850ed5c4de --- /dev/null +++ b/apps/desktop/src/components/config/__tests__/DriverStoreDialog.externalUpdateSync.spec.ts @@ -0,0 +1,147 @@ +// @vitest-environment happy-dom + +import { createApp, defineComponent, h, nextTick, type App } from "vue"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { AgentDriverInfo } from "@/lib/backend/tauri"; +import type { JdbcPluginStatus } from "@/types/database"; +import { COMPONENT_DRIVER_UPDATES_CHANGED_EVENT } from "@/lib/updates/componentUpdateEvents"; + +const mocks = vi.hoisted(() => ({ + listInstalledAgentsLocal: vi.fn(), + listInstalledAgents: vi.fn(), + invalidateAgentRegistryCache: vi.fn(), + getAgentJavaRuntimeConfig: vi.fn(), + getDriverStoreUsage: vi.fn(), + getDriverStorePath: vi.fn(), + listenAgentInstallProgress: vi.fn(), + listJdbcDrivers: vi.fn(), + listJdbcMavenBundles: vi.fn(), + listJdbcLocalBundles: vi.fn(), + jdbcPluginStatus: vi.fn(), + toast: vi.fn(), +})); + +vi.mock("@/lib/backend/api", () => mocks); +vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) })); +vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); +vi.mock("@/stores/settingsStore", async () => { + const { reactive } = await import("vue"); + return { + useSettingsStore: () => + reactive({ + desktopSettings: { driver_store_dir: null, plugin_store_dir: null, agent_store_dir: null }, + editorSettings: { updateDownloadSource: "official" }, + updateEditorSettings: vi.fn(), + }), + }; +}); +vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) })); + +function passthrough(name: string, tag = "div") { + return defineComponent({ + name, + inheritAttrs: false, + setup(_props, { attrs, slots }) { + return () => h(tag, attrs, [slots.default?.(), slots.content?.(), slots.header?.(), slots.footer?.()]); + }, + }); +} + +vi.mock("@/components/ui/button", () => ({ Button: passthrough("Button", "button") })); +vi.mock("@/components/ui/input", () => ({ Input: passthrough("Input", "input") })); +vi.mock("@/components/ui/label", () => ({ Label: passthrough("Label", "label") })); +vi.mock("@/components/ui/badge", () => ({ Badge: passthrough("Badge", "span") })); +vi.mock("@/components/ui/select", () => ({ Select: passthrough("Select"), SelectContent: passthrough("SelectContent"), SelectItem: passthrough("SelectItem"), SelectTrigger: passthrough("SelectTrigger"), SelectValue: passthrough("SelectValue") })); +vi.mock("@/components/ui/tooltip", () => ({ Tooltip: passthrough("Tooltip"), TooltipContent: passthrough("TooltipContent"), TooltipTrigger: passthrough("TooltipTrigger") })); +vi.mock("@/components/ui/tabs", () => ({ Tabs: passthrough("Tabs"), TabsContent: passthrough("TabsContent"), TabsList: passthrough("TabsList"), TabsTrigger: passthrough("TabsTrigger") })); +vi.mock("@/components/config/DriverInstallProgressCircle.vue", () => ({ default: passthrough("DriverInstallProgressCircle") })); +vi.mock("@/components/config/DriverStoreAgentRow.vue", () => ({ default: passthrough("DriverStoreAgentRow") })); +vi.mock("@/components/config/AgentOfflineExportDialog.vue", () => ({ default: passthrough("AgentOfflineExportDialog") })); + +import DriverStoreDialog from "@/components/config/DriverStoreDialog.vue"; + +function driver(updateAvailable: boolean): AgentDriverInfo { + return { + db_type: "oracle", + label: "Oracle", + version: "2.0.0", + size: 1, + installed: true, + installed_version: updateAvailable ? "1.0.0" : "2.0.0", + update_available: updateAvailable, + jre: "21", + jre_installed: true, + }; +} + +function jdbcStatus(updateAvailable: boolean): JdbcPluginStatus { + return { + installed: true, + version: updateAvailable ? "1.0.0" : "2.0.0", + compatible: true, + latest_version: "2.0.0", + update_available: updateAvailable, + path: "/tmp/jdbc", + }; +} + +async function flushUi() { + for (let index = 0; index < 10; index++) { + await Promise.resolve(); + await nextTick(); + } +} + +let app: App; +let host: HTMLElement; +let updateCountChange: ReturnType; + +beforeEach(async () => { + vi.resetAllMocks(); + mocks.listInstalledAgentsLocal.mockResolvedValue([driver(true)]); + mocks.listInstalledAgents.mockResolvedValue([driver(true)]); + mocks.invalidateAgentRegistryCache.mockResolvedValue(undefined); + mocks.getAgentJavaRuntimeConfig.mockResolvedValue({ mode: "managed", custom_java_path: null }); + mocks.getDriverStoreUsage.mockResolvedValue(null); + mocks.getDriverStorePath.mockResolvedValue({ driver_store_dir: "", plugin_store_dir: "", agent_store_dir: "", plugins_dir: "", agents_dir: "" }); + mocks.listenAgentInstallProgress.mockResolvedValue(vi.fn()); + mocks.listJdbcDrivers.mockResolvedValue([]); + mocks.listJdbcMavenBundles.mockResolvedValue([]); + mocks.listJdbcLocalBundles.mockResolvedValue([]); + mocks.jdbcPluginStatus.mockResolvedValue(jdbcStatus(true)); + + host = document.createElement("div"); + document.body.append(host); + updateCountChange = vi.fn(); + app = createApp({ + render: () => + h(DriverStoreDialog, { + activeTab: "agent", + onUpdateCountChange: updateCountChange, + }), + }); + app.mount(host); + await flushUi(); +}); + +afterEach(() => { + app.unmount(); + host.remove(); +}); + +describe("DriverStoreDialog external component update sync", () => { + it("refreshes local agent and JDBC state when the update center changes drivers", async () => { + updateCountChange.mockClear(); + mocks.listInstalledAgents.mockResolvedValueOnce([driver(false)]); + mocks.jdbcPluginStatus.mockResolvedValueOnce(jdbcStatus(false)); + + window.dispatchEvent(new Event(COMPONENT_DRIVER_UPDATES_CHANGED_EVENT)); + await flushUi(); + + expect(mocks.invalidateAgentRegistryCache).toHaveBeenCalledTimes(2); + expect(mocks.listInstalledAgents).toHaveBeenCalledTimes(2); + expect(mocks.listJdbcDrivers).toHaveBeenCalledTimes(2); + expect(mocks.jdbcPluginStatus).toHaveBeenCalledTimes(2); + expect(updateCountChange).toHaveBeenLastCalledWith(0); + }); +}); diff --git a/apps/desktop/src/lib/updates/__tests__/componentUpdateEvents.spec.ts b/apps/desktop/src/lib/updates/__tests__/componentUpdateEvents.spec.ts index faf8ab9b53..8fa7565b3f 100644 --- a/apps/desktop/src/lib/updates/__tests__/componentUpdateEvents.spec.ts +++ b/apps/desktop/src/lib/updates/__tests__/componentUpdateEvents.spec.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { describe, expect, it, vi } from "vitest"; -import { COMPONENT_PLUGINS_UPDATED_EVENT, COMPONENT_UPDATES_CHANGED_EVENT, notifyComponentPluginsUpdated, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; +import { COMPONENT_DRIVER_UPDATES_CHANGED_EVENT, COMPONENT_PLUGINS_UPDATED_EVENT, COMPONENT_UPDATES_CHANGED_EVENT, notifyComponentDriverUpdatesChanged, notifyComponentPluginsUpdated, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; describe("component update events", () => { it("notifies plugin surfaces after component-level plugin updates", () => { @@ -24,10 +24,21 @@ describe("component update events", () => { window.removeEventListener(COMPONENT_UPDATES_CHANGED_EVENT, listener); }); + it("notifies the driver store when component updates change driver state", () => { + const listener = vi.fn(); + window.addEventListener(COMPONENT_DRIVER_UPDATES_CHANGED_EVENT, listener); + + notifyComponentDriverUpdatesChanged(); + + expect(listener).toHaveBeenCalledOnce(); + window.removeEventListener(COMPONENT_DRIVER_UPDATES_CHANGED_EVENT, listener); + }); + it("is inert when the store runs outside a browser environment", () => { vi.stubGlobal("window", undefined); try { expect(() => notifyComponentPluginsUpdated()).not.toThrow(); + expect(() => notifyComponentDriverUpdatesChanged()).not.toThrow(); expect(() => notifyComponentUpdatesChanged()).not.toThrow(); } finally { vi.unstubAllGlobals(); diff --git a/apps/desktop/src/lib/updates/componentUpdateEvents.ts b/apps/desktop/src/lib/updates/componentUpdateEvents.ts index c626b972f0..e05bbbd072 100644 --- a/apps/desktop/src/lib/updates/componentUpdateEvents.ts +++ b/apps/desktop/src/lib/updates/componentUpdateEvents.ts @@ -1,4 +1,5 @@ export const COMPONENT_PLUGINS_UPDATED_EVENT = "dbx:component-plugins-updated"; +export const COMPONENT_DRIVER_UPDATES_CHANGED_EVENT = "dbx:component-driver-updates-changed"; export const COMPONENT_UPDATES_CHANGED_EVENT = "dbx:component-updates-changed"; export function notifyComponentPluginsUpdated(): void { @@ -6,6 +7,11 @@ export function notifyComponentPluginsUpdated(): void { window.dispatchEvent(new Event(COMPONENT_PLUGINS_UPDATED_EVENT)); } +export function notifyComponentDriverUpdatesChanged(): void { + if (typeof window === "undefined") return; + window.dispatchEvent(new Event(COMPONENT_DRIVER_UPDATES_CHANGED_EVENT)); +} + export function notifyComponentUpdatesChanged(): void { if (typeof window === "undefined") return; window.dispatchEvent(new Event(COMPONENT_UPDATES_CHANGED_EVENT));