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

+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 {