fix(updates): sync driver store state after update center runs

This commit is contained in:
zipg
2026-09-30 19:46:49 +08:00
committed by GitHub
parent 7745834955
commit 5017aa7bb9
5 changed files with 174 additions and 3 deletions
+2 -1
View File
@@ -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<ReturnType<typeof component
// Only a clean refresh is authoritative; a failed registry check must not clear stale toolbar state.
if (result.failed.length === 0) syncToolbarComponentUpdateState();
if (result.plugins > 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);
@@ -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();
});
@@ -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<typeof vi.fn>;
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);
});
});
@@ -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();
@@ -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));