fix(updates): auto-continue client upgrade on update-all

This commit is contained in:
zipg authored and GitHub committed 2026-09-23 14:43:14 +08:00
1 parent 6471e37e5f
commit da95bd435f
10 files changed
+200 -61

No files matched your search

+20 -11
View File
@@ -33,6 +33,7 @@ import type { PluginUpdateBlock } from "@/composables/useComponentUpdates";
import { COMPONENT_UPDATES_CHANGED_EVENT, notifyComponentPluginsUpdated, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
import { driverStoreUpdateBadgeCount, showMcpUpdateBadge, showToolbarUpdateAction } from "@/lib/updates/updateBadges";
import {
continuePreparedAppUpdate,
hasPendingComponentUpdatesAfterAppRestart,
markPendingComponentUpdatesAfterAppUpdate,
resolveUpdateAllAction,
@@ -40,6 +41,7 @@ import {
runPendingComponentUpdatePlan,
shouldCloseUpdateCenterAfterComponentUpdate,
takePendingComponentUpdatesAfterAppRestart,
updateBlockerLabels,
type PendingComponentUpdatePlan,
} from "@/lib/updates/componentUpdateOrchestration";
import { isUpdatePreviewMockEnabled } from "@/lib/updates/updatePreviewMock";
@@ -1289,7 +1291,9 @@ function reportComponentUpdateResult(result: Awaited<ReturnType<typeof component
if (result.failed.length === 0) syncToolbarComponentUpdateState();
if (result.plugins > 0) notifyComponentPluginsUpdated();
if (updatedComponents.length) toast(t("updates.componentsAutoUpdated", { components: updatedComponents.join(t("updates.componentListSeparator")) }));
if (result.skippedDrivers > 0) toast(t("updates.componentsAutoUpdateSkipped"), 6000);
if (result.blockedDrivers.length) {
toast(t("driverStore.driverUpdateBlocked", { labels: updateBlockerLabels(result.blockedDrivers).join(", ") }), 8000);
} else if (result.skippedDrivers > 0) toast(t("updates.componentsAutoUpdateSkipped"), 6000);
const otherFailureCount = result.failed.length - result.blockedPlugins.length;
const failureMessages = [result.blockedPlugins.map(pluginUpdateBlockMessage).join("\n"), otherFailureCount > 0 ? t("updates.componentsAutoUpdateFailed", { count: otherFailureCount }) : ""].filter(Boolean);
if (failureMessages.length) toast(failureMessages.join("\n"), 8000);
@@ -1342,6 +1346,19 @@ function availableComponentUpdateCategories(): ComponentUpdateCategory[] {
return categories;
}
function continueAppUpdateWithComponents(categories: ComponentUpdateCategory[]) {
return continuePreparedAppUpdate({
hasComponentUpdates: categories.length > 0,
restartOnly: updateReady.value,
rememberComponentUpdates: () => rememberComponentUpdatesForRestartedApp({ kind: "manual", categories }),
installComponents: async () => {
reportComponentUpdateResult(await componentUpdates.installCategories(categories));
},
installDownloadedUpdate,
restartApp,
});
}
async function updateAllAvailable() {
if (updatingAllUpdates.value) return;
updatingAllUpdates.value = true;
@@ -1359,17 +1376,9 @@ async function updateAllAvailable() {
reportComponentUpdateResult(await componentUpdates.installCategories(categories));
return;
}
if (action === "defer-components") {
const remembered = await rememberComponentUpdatesForRestartedApp({ kind: "manual", categories });
if (remembered) {
toast(t("settings.updateRestartHint"), 6000);
return;
}
reportComponentUpdateResult(await componentUpdates.installCategories(categories));
return;
}
if (action === "install-app") return continueAppUpdateWithComponents(categories);
if (action === "download-app") await downloadUpdateInBackground();
if (updateDownloaded.value || updateReady.value) await rememberComponentUpdatesForRestartedApp({ kind: "manual", categories });
if (updateDownloaded.value || updateReady.value) await continueAppUpdateWithComponents(categories);
} finally {
updatingAllUpdates.value = false;
}
@@ -41,6 +41,7 @@ import { runAgentOfflineExportAction } from "@/lib/driverStore/agentOfflineExpor
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 { updateBlockerLabels } from "@/lib/updates/componentUpdateOrchestration";
const { t } = useI18n();
const { toast } = useToast();
@@ -53,6 +54,10 @@ function backendError(e: unknown): string {
return translateBackendError(t, message);
}
function driverUpdateBlockedMessage(blockers: Awaited<ReturnType<typeof api.checkAgentUpdateBlockers>>): string {
return updateBlockerLabels(blockers).join(", ");
}
const props = withDefaults(
defineProps<{
updateNotificationsEnabled?: boolean;
@@ -504,7 +509,7 @@ async function runDriverInstall(dbType: string) {
}
const blockers = await api.checkAgentUpdateBlockers([dbType]);
if (blockers.length > 0) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
toast(t("driverStore.driverUpdateBlocked", { labels: driverUpdateBlockedMessage(blockers) }));
return;
}
const operationId = uuid();
@@ -548,7 +553,7 @@ async function upgradeAll() {
if (updatableDbTypes.length === 0) return;
const blockers = await api.checkAgentUpdateBlockers(updatableDbTypes);
if (blockers.length > 0) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
toast(t("driverStore.driverUpdateBlocked", { labels: driverUpdateBlockedMessage(blockers) }));
return;
}
// Do not expose Cancel until the backend operation has registered its token.
@@ -623,7 +628,7 @@ async function uninstallDriver(dbType: string) {
}
const blockers = await api.checkAgentUpdateBlockers([dbType]);
if (blockers.length > 0) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
toast(t("driverStore.driverUpdateBlocked", { labels: driverUpdateBlockedMessage(blockers) }));
return;
}
await api.uninstallAgent(dbType);
@@ -791,7 +796,7 @@ async function importDriverFile(driver: AgentDriverInfo) {
}
const blockers = await api.checkAgentUpdateBlockers([dbType]);
if (blockers.length > 0) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
toast(t("driverStore.driverUpdateBlocked", { labels: driverUpdateBlockedMessage(blockers) }));
return;
}
const label = driverLabel(dbType);
@@ -131,7 +131,7 @@ describe("useComponentUpdates", () => {
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
expect(mocks.installMarketplacePlugin).toHaveBeenCalledWith({ repositoryId: "official", pluginId: "example", version: "1.1.0" });
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [], blockedPlugins: [] });
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, blockedDrivers: [], failed: [], blockedPlugins: [] });
});
it("shares one update operation between automatic and manual callers", async () => {
@@ -172,7 +172,7 @@ describe("useComponentUpdates", () => {
expect(mocks.installJdbcPlugin).not.toHaveBeenCalled();
expect(mocks.installMcpServer).not.toHaveBeenCalled();
expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled();
expect(result).toEqual({ drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [], blockedPlugins: [] });
expect(result).toEqual({ drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, blockedDrivers: [], failed: [], blockedPlugins: [] });
});
it("installs every manually selected category when no DBX update exists and automatic updates are disabled", async () => {
@@ -190,7 +190,7 @@ describe("useComponentUpdates", () => {
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [], blockedPlugins: [] });
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, blockedDrivers: [], failed: [], blockedPlugins: [] });
});
it("resumes a persisted manual update-all plan after restart while automatic updates are disabled", async () => {
@@ -213,7 +213,7 @@ describe("useComponentUpdates", () => {
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [], blockedPlugins: [] });
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, blockedDrivers: [], failed: [], blockedPlugins: [] });
});
it("keeps the toolbar update action hidden while a persisted restart plan is still installing", async () => {
@@ -247,7 +247,7 @@ describe("useComponentUpdates", () => {
});
it("skips blocked agent updates while allowing other component updates", async () => {
mocks.checkAgentUpdateBlockers.mockResolvedValue([{ db_type: "mysql", label: "MySQL" }]);
mocks.checkAgentUpdateBlockers.mockResolvedValue([{ db_type: "mysql", label: "MySQL", connections: ["生产 MySQL", "报表 MySQL"] }]);
const updates = useComponentUpdates({ isDesktop: true });
const result = await updates.autoUpdateEnabledComponents();
@@ -257,6 +257,7 @@ describe("useComponentUpdates", () => {
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
expect(result.skippedDrivers).toBe(1);
expect(result.blockedDrivers).toEqual([{ db_type: "mysql", label: "MySQL", connections: ["生产 MySQL", "报表 MySQL"] }]);
});
it("does not install from stale state when update detection fails", async () => {
@@ -302,7 +303,7 @@ describe("useComponentUpdates", () => {
const result = await updates.installCategory("jdbc");
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
expect(result).toEqual({ drivers: 0, jdbc: true, mcp: false, plugins: 0, skippedDrivers: 0, failed: [], blockedPlugins: [] });
expect(result).toEqual({ drivers: 0, jdbc: true, mcp: false, plugins: 0, skippedDrivers: 0, blockedDrivers: [], failed: [], blockedPlugins: [] });
});
it("reports the blocking connections when a K8S plugin update cannot start", async () => {
@@ -6,7 +6,7 @@ import { mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus";
import { useSettingsStore } from "@/stores/settingsStore";
import { isUpdatePreviewMockEnabled, previewDriverUpdates, previewJdbcUpdate, previewMcpUpdate, previewPluginUpdates } from "@/lib/updates/updatePreviewMock";
import type { ComponentUpdateCategory } from "@/lib/updates/componentUpdateOrchestration";
import type { AgentDriverInfo, McpServerStatus } from "@/lib/backend/tauri";
import type { AgentDriverInfo, AgentUpdateBlocker, McpServerStatus } from "@/lib/backend/tauri";
import type { JdbcPluginStatus } from "@/types/database";
export type { ComponentUpdateCategory } from "@/lib/updates/componentUpdateOrchestration";
@@ -19,6 +19,7 @@ export interface ComponentUpdateResult {
mcp: boolean;
plugins: number;
skippedDrivers: number;
blockedDrivers: AgentUpdateBlocker[];
failed: string[];
blockedPlugins: PluginUpdateBlock[];
}
@@ -28,7 +29,7 @@ interface ComponentUpdateRefreshOptions {
}
function emptyResult(): ComponentUpdateResult {
return { drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [], blockedPlugins: [] };
return { drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, blockedDrivers: [], failed: [], blockedPlugins: [] };
}
function pluginUpdateBlock(pluginName: string, message: string): PluginUpdateBlock | null {
@@ -139,6 +140,7 @@ export function useComponentUpdates(options: { isDesktop: boolean }) {
const blockers = await api.checkAgentUpdateBlockers(updatable);
if (blockers.length) {
result.skippedDrivers = blockers.length;
result.blockedDrivers = blockers;
return;
}
const upgraded = await api.upgradeAllAgents();
+1
View File
@@ -204,6 +204,7 @@ export interface UpgradeAllAgentDriversResult {
export interface AgentUpdateBlocker {
db_type: string;
label: string;
connections: string[];
}
export type AgentOfflineArtifactKind = "jar" | "native";
@@ -3,6 +3,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
clearPendingComponentUpdatesAfterAppUpdate,
continuePreparedAppUpdate,
hasPendingComponentUpdatesAfterAppRestart,
markPendingComponentUpdatesAfterAppUpdate,
resolveUpdateAllAction,
@@ -10,6 +11,7 @@ import {
runPendingComponentUpdatePlan,
shouldCloseUpdateCenterAfterComponentUpdate,
takePendingComponentUpdatesAfterAppRestart,
updateBlockerLabels,
} from "@/lib/updates/componentUpdateOrchestration";
describe("component update orchestration", () => {
@@ -17,6 +19,16 @@ describe("component update orchestration", () => {
localStorage.clear();
});
it("lists blocked connection names and falls back to the driver label", () => {
expect(
updateBlockerLabels([
{ label: "MySQL", connections: ["生产 MySQL", "报表 MySQL"] },
{ label: "Oracle", connections: [] },
{ label: "DuckDB", connections: ["生产 MySQL"] },
]),
).toEqual(["生产 MySQL", "报表 MySQL", "Oracle"]);
});
it("keeps pending component updates across a failed or ordinary startup", () => {
expect(markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17")).toBe(true);
@@ -162,19 +174,74 @@ describe("component update orchestration", () => {
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")?.plan).toEqual({ kind: "auto" });
});
it("downloads DBX before deferring component updates", () => {
it("downloads DBX before installing it with component updates", () => {
expect(resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: true, appUpdatePrepared: false, hasComponentUpdates: true })).toBe("download-app");
expect(resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: true, appUpdatePrepared: true, hasComponentUpdates: true })).toBe("defer-components");
expect(resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: true, appUpdatePrepared: true, hasComponentUpdates: true })).toBe("install-app");
});
it("persists the selected component categories when the DBX package is already prepared", () => {
const action = resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: true, appUpdatePrepared: true, hasComponentUpdates: true });
expect(action).toBe("defer-components");
expect(action).toBe("install-app");
expect(markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17", { kind: "manual", categories: ["drivers", "mcp"] })).toBe(true);
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")?.plan).toEqual({ kind: "manual", categories: ["drivers", "mcp"] });
});
it("continues from update all into the prepared app install", async () => {
const rememberComponentUpdates = vi.fn(async () => true);
const installComponents = vi.fn(async () => {});
const installDownloadedUpdate = vi.fn(async () => {});
const restartApp = vi.fn(async () => {});
await continuePreparedAppUpdate({
hasComponentUpdates: true,
restartOnly: false,
rememberComponentUpdates,
installComponents,
installDownloadedUpdate,
restartApp,
});
expect(rememberComponentUpdates).toHaveBeenCalledOnce();
expect(installDownloadedUpdate).toHaveBeenCalledOnce();
expect(restartApp).not.toHaveBeenCalled();
expect(installComponents).not.toHaveBeenCalled();
});
it("continues from update all into restart when the app update is already staged", async () => {
const restartApp = vi.fn(async () => {});
await continuePreparedAppUpdate({
hasComponentUpdates: true,
restartOnly: true,
rememberComponentUpdates: async () => true,
installComponents: async () => {},
installDownloadedUpdate: async () => {},
restartApp,
});
expect(restartApp).toHaveBeenCalledOnce();
});
it("falls back to component updates without restarting when the restart plan cannot be persisted", async () => {
const installComponents = vi.fn(async () => {});
const installDownloadedUpdate = vi.fn(async () => {});
const restartApp = vi.fn(async () => {});
await continuePreparedAppUpdate({
hasComponentUpdates: true,
restartOnly: false,
rememberComponentUpdates: async () => false,
installComponents,
installDownloadedUpdate,
restartApp,
});
expect(installComponents).toHaveBeenCalledOnce();
expect(installDownloadedUpdate).not.toHaveBeenCalled();
expect(restartApp).not.toHaveBeenCalled();
});
it("updates components immediately when no DBX update exists or the package cannot be installed", () => {
expect(resolveUpdateAllAction({ hasAppUpdate: false, appUpdateCanInstall: true, appUpdatePrepared: false, hasComponentUpdates: true })).toBe("update-components");
expect(resolveUpdateAllAction({ hasAppUpdate: true, appUpdateCanInstall: false, appUpdatePrepared: false, hasComponentUpdates: true })).toBe("update-components");
@@ -14,7 +14,11 @@ export interface PendingComponentUpdates {
plan: PendingComponentUpdatePlan;
}
export type UpdateAllAction = "none" | "download-app" | "defer-components" | "update-components";
export type UpdateAllAction = "none" | "download-app" | "install-app" | "update-components";
export function updateBlockerLabels(blockers: Array<{ label: string; connections?: string[] }>): string[] {
return [...new Set(blockers.flatMap((blocker) => (blocker.connections?.length ? blocker.connections : [blocker.label])))];
}
function normalizeVersion(version: string): string {
return version.trim().replace(/^[vV]/, "");
@@ -112,7 +116,16 @@ export function takePendingComponentUpdatesAfterAppRestart(currentVersion: strin
export function resolveUpdateAllAction(options: { hasAppUpdate: boolean; appUpdateCanInstall: boolean; appUpdatePrepared: boolean; hasComponentUpdates: boolean }): UpdateAllAction {
if (!options.hasAppUpdate || !options.appUpdateCanInstall) return options.hasComponentUpdates ? "update-components" : "none";
return options.appUpdatePrepared ? "defer-components" : "download-app";
return options.appUpdatePrepared ? "install-app" : "download-app";
}
export async function continuePreparedAppUpdate(options: { hasComponentUpdates: boolean; restartOnly: boolean; rememberComponentUpdates: () => Promise<boolean>; installComponents: () => Promise<void>; installDownloadedUpdate: () => Promise<void>; restartApp: () => Promise<void> }): Promise<void> {
if (options.hasComponentUpdates && !(await options.rememberComponentUpdates())) {
await options.installComponents();
return;
}
if (options.restartOnly) await options.restartApp();
else await options.installDownloadedUpdate();
}
export function shouldCloseUpdateCenterAfterComponentUpdate(options: { failedCount: number; skippedDriverCount: number; hasAppUpdate: boolean; remainingComponentUpdateCount: number }): boolean {