fix(desktop): allow auto-restart even when the instance is behind
Stop holding downloaded desktop updates until the connected instance catches up. When the client is newer, system info shows a small hint that some features may not work yet and that the instance can update automatically each day or be updated manually.
This commit is contained in:
1 parent
e154d73455
commit
e8ed6b1019
16 files changed
+172
-159
No files matched your search
@@ -78,13 +78,13 @@ describe("desktop update flow", () => {
|
||||
expect(notebookPaneSource).toContain('className="flex items-center gap-1"');
|
||||
});
|
||||
|
||||
test("holds in-app restart installation while the GitHub release is newer than the instance", () => {
|
||||
expect(mainSource).toContain("shouldHoldAutoRestartUpdate(downloadedUpdateVersion, instanceVersion)");
|
||||
expect(mainSource).toContain("holdAutoRestartUpdate(downloadedUpdateVersion)");
|
||||
expect(mainSource).toContain('writeDiagnostic("update.held-for-instance"');
|
||||
expect(mainSource).toContain("autoUpdater.autoInstallOnAppQuit = false");
|
||||
expect(mainSource).toContain("releaseHeldAutoRestartUpdate()");
|
||||
expect(mainSource).toContain("${configuredApiBaseUrl}/api/release");
|
||||
expect(mainSource).toContain("heldUpdateVersion ||");
|
||||
test("does not hold auto-restart while the GitHub release is newer than the instance", () => {
|
||||
expect(mainSource).not.toContain("shouldHoldAutoRestartUpdate");
|
||||
expect(mainSource).not.toContain("holdAutoRestartUpdate");
|
||||
expect(mainSource).not.toContain("heldUpdateVersion");
|
||||
expect(mainSource).not.toContain("update.held-for-instance");
|
||||
expect(mainSource).toContain("promptForDownloadedUpdate(downloadedUpdateVersion)");
|
||||
expect(systemInfoSource).toContain('t("systemInfo.clientAheadOfInstance")');
|
||||
expect(systemInfoSource).toContain("isClientAheadOfInstance");
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
fetchTrustedWindowsUpdate,
|
||||
verifyDownloadedWindowsUpdate,
|
||||
} from "./windows-update-trust.mjs";
|
||||
import { instanceReleaseVersionFromPayload, shouldHoldAutoRestartUpdate } from "./instance-update-gate.mjs";
|
||||
import electronUpdater from "electron-updater";
|
||||
import { createPluginPublicNetworkRuntime } from "./plugin-public-network.mjs";
|
||||
import { shouldQuitAfterAllWindowsClosed } from "./window-lifecycle.mjs";
|
||||
@@ -116,7 +115,6 @@ let updateDownloadInFlight = null;
|
||||
let updateCheckTimer = null;
|
||||
let lastUpdateCheckAt = 0;
|
||||
let downloadedUpdateVersion = null;
|
||||
let heldUpdateVersion = null;
|
||||
let promptedUpdateVersion = null;
|
||||
let trustedWindowsUpdate = null;
|
||||
let windowsDownloadedUpdateVerified = false;
|
||||
@@ -808,52 +806,9 @@ const publishDesktopUpdateStatus = () => {
|
||||
mainWindow.webContents.send("desktop:update-status-changed", desktopUpdateStatus());
|
||||
};
|
||||
|
||||
const readInstanceReleaseVersion = async () => {
|
||||
if (!configuredApiBaseUrl) return null;
|
||||
try {
|
||||
const response = await net.fetch(`${configuredApiBaseUrl}/api/release`);
|
||||
if (!response.ok) return null;
|
||||
return instanceReleaseVersionFromPayload(await response.json());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const holdAutoRestartUpdate = async (version) => {
|
||||
heldUpdateVersion = version || "unknown";
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
updateState = "available";
|
||||
downloadedUpdateVersion = version || downloadedUpdateVersion;
|
||||
refreshTrayMenu();
|
||||
publishDesktopUpdateStatus();
|
||||
await writeDiagnostic("update.held-for-instance", { version: heldUpdateVersion });
|
||||
};
|
||||
|
||||
const releaseHeldAutoRestartUpdate = async () => {
|
||||
if (!heldUpdateVersion || linuxUpdateTestMode) return false;
|
||||
const instanceVersion = await readInstanceReleaseVersion();
|
||||
if (shouldHoldAutoRestartUpdate(heldUpdateVersion, instanceVersion)) return false;
|
||||
const version = heldUpdateVersion === "unknown" ? downloadedUpdateVersion : heldUpdateVersion;
|
||||
heldUpdateVersion = null;
|
||||
if (process.platform !== "win32" || windowsDownloadedUpdateVerified) {
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
}
|
||||
updateState = "downloaded";
|
||||
downloadedUpdateVersion = version || downloadedUpdateVersion;
|
||||
refreshTrayMenu();
|
||||
publishDesktopUpdateStatus();
|
||||
await writeDiagnostic("update.released-for-instance", { version: downloadedUpdateVersion });
|
||||
await promptForDownloadedUpdate(downloadedUpdateVersion).catch((error) => {
|
||||
promptedUpdateVersion = null;
|
||||
void writeDiagnostic("update.prompt-failed", { message: error.message });
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const installDownloadedUpdate = () => {
|
||||
if (
|
||||
updateState !== "downloaded" ||
|
||||
heldUpdateVersion ||
|
||||
(process.platform === "win32" && !windowsDownloadedUpdateVerified)
|
||||
) return { started: false };
|
||||
// The normal window close handler hides the app. Mark this as a real quit
|
||||
@@ -926,11 +881,8 @@ const checkForDesktopUpdate = (reason, { force = false, throwOnError = false } =
|
||||
if (!force && now - lastUpdateCheckAt < updateCheckFocusThrottleMs) return Promise.resolve(null);
|
||||
lastUpdateCheckAt = now;
|
||||
void writeDiagnostic("update.check-started", { reason });
|
||||
updateCheckInFlight = Promise.resolve()
|
||||
.then(() => releaseHeldAutoRestartUpdate())
|
||||
.then(async (released) => {
|
||||
if (released || updateState === "downloaded") return null;
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
updateCheckInFlight = autoUpdater.checkForUpdates()
|
||||
.then(async (result) => {
|
||||
if (process.platform === "win32" && result?.isUpdateAvailable) {
|
||||
trustedWindowsUpdate = await fetchTrustedWindowsUpdate({
|
||||
version: result.updateInfo.version,
|
||||
@@ -950,14 +902,12 @@ const checkForDesktopUpdate = (reason, { force = false, throwOnError = false } =
|
||||
return result;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
if (!heldUpdateVersion) {
|
||||
updateState = "idle";
|
||||
downloadedUpdateVersion = null;
|
||||
trustedWindowsUpdate = null;
|
||||
windowsDownloadedUpdateVerified = false;
|
||||
refreshTrayMenu();
|
||||
publishDesktopUpdateStatus();
|
||||
}
|
||||
updateState = "idle";
|
||||
downloadedUpdateVersion = null;
|
||||
trustedWindowsUpdate = null;
|
||||
windowsDownloadedUpdateVerified = false;
|
||||
refreshTrayMenu();
|
||||
publishDesktopUpdateStatus();
|
||||
await writeDiagnostic("update.check-failed", { reason, message: error.message });
|
||||
throw error;
|
||||
})
|
||||
@@ -995,7 +945,6 @@ const configureAutoUpdater = () => {
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
updateState = "idle";
|
||||
downloadedUpdateVersion = null;
|
||||
heldUpdateVersion = null;
|
||||
trustedWindowsUpdate = null;
|
||||
windowsDownloadedUpdateVerified = false;
|
||||
refreshTrayMenu();
|
||||
@@ -1019,16 +968,8 @@ const configureAutoUpdater = () => {
|
||||
windowsDownloadedUpdateVerified = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
}
|
||||
downloadedUpdateVersion = info?.version || downloadedUpdateVersion;
|
||||
if (!linuxUpdateTestMode) {
|
||||
const instanceVersion = await readInstanceReleaseVersion();
|
||||
if (shouldHoldAutoRestartUpdate(downloadedUpdateVersion, instanceVersion)) {
|
||||
await holdAutoRestartUpdate(downloadedUpdateVersion);
|
||||
return;
|
||||
}
|
||||
}
|
||||
updateState = "downloaded";
|
||||
heldUpdateVersion = null;
|
||||
downloadedUpdateVersion = info?.version || downloadedUpdateVersion;
|
||||
refreshTrayMenu();
|
||||
publishDesktopUpdateStatus();
|
||||
await writeDiagnostic("update.downloaded", { version: downloadedUpdateVersion });
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { instanceReleaseVersionFromPayload, shouldHoldAutoRestartUpdate } from "./instance-update-gate.mjs";
|
||||
|
||||
describe("desktop auto-restart update gate", () => {
|
||||
test("holds only when the GitHub release is newer than the connected instance", () => {
|
||||
expect(shouldHoldAutoRestartUpdate("1.64.0", "1.63.2")).toBe(true);
|
||||
expect(shouldHoldAutoRestartUpdate("v1.64.0", "v1.64.0")).toBe(false);
|
||||
expect(shouldHoldAutoRestartUpdate("1.63.2", "1.64.0")).toBe(false);
|
||||
expect(shouldHoldAutoRestartUpdate("1.64.0+12", "1.64.0")).toBe(false);
|
||||
});
|
||||
|
||||
test("does not hold when the instance version is unknown", () => {
|
||||
expect(shouldHoldAutoRestartUpdate("1.64.0", null)).toBe(false);
|
||||
expect(shouldHoldAutoRestartUpdate("1.64.0", "")).toBe(false);
|
||||
expect(shouldHoldAutoRestartUpdate("1.64.0", "local")).toBe(false);
|
||||
expect(shouldHoldAutoRestartUpdate("", "1.63.2")).toBe(false);
|
||||
});
|
||||
|
||||
test("reads the public instance release payload", () => {
|
||||
expect(instanceReleaseVersionFromPayload({ version: "1.63.2+4" })).toBe("1.63.2+4");
|
||||
expect(instanceReleaseVersionFromPayload({ version: " v1.64.0 " })).toBe("v1.64.0");
|
||||
expect(instanceReleaseVersionFromPayload({})).toBeNull();
|
||||
expect(instanceReleaseVersionFromPayload(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -462,7 +462,8 @@ struct SettingsView: View {
|
||||
title: env.preferences.t("云端实例", en: "Cloud instance"),
|
||||
description: env.preferences.t("当前连接实例的版本与部署环境。", en: "Version and deployment environment for the connected instance."),
|
||||
icon: "cloud",
|
||||
items: cloudSystemInfoItems
|
||||
items: cloudSystemInfoItems,
|
||||
notice: clientAheadOfInstanceNotice
|
||||
)
|
||||
|
||||
systemInfoGroup(
|
||||
@@ -516,6 +517,36 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var clientAheadOfInstanceNotice: String? {
|
||||
let clientVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
guard let clientVersion, let instanceVersion, Self.isClient(clientVersion, aheadOfInstance: instanceVersion) else {
|
||||
return nil
|
||||
}
|
||||
return env.preferences.t(
|
||||
"当前客户端版本高于云端实例,部分功能可能暂不可用。可等待每天自动更新,也可手动更新实例。",
|
||||
en: "This client is newer than the connected cloud instance, so some features may not work yet. You can wait for the daily automatic instance update, or update the instance manually."
|
||||
)
|
||||
}
|
||||
|
||||
private static func isClient(_ clientVersion: String, aheadOfInstance instanceVersion: String) -> Bool {
|
||||
func core(_ value: String) -> [Int]? {
|
||||
let pattern = #"^v?(\d+)\.(\d+)\.(\d+)"#
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { return nil }
|
||||
let range = NSRange(value.startIndex..., in: value)
|
||||
guard let match = regex.firstMatch(in: value, range: range) else { return nil }
|
||||
let numbers = (1...3).compactMap { index -> Int? in
|
||||
guard let part = Range(match.range(at: index), in: value) else { return nil }
|
||||
return Int(value[part])
|
||||
}
|
||||
return numbers.count == 3 ? numbers : nil
|
||||
}
|
||||
guard let client = core(clientVersion), let instance = core(instanceVersion) else { return false }
|
||||
for index in 0..<3 where client[index] != instance[index] {
|
||||
return client[index] > instance[index]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var clientSystemInfoItems: [(label: String, value: String)] {
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "—"
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "—"
|
||||
@@ -720,7 +751,8 @@ struct SettingsView: View {
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
items: [(label: String, value: String)]
|
||||
items: [(label: String, value: String)],
|
||||
notice: String? = nil
|
||||
) -> some View {
|
||||
settingsGroup(title: title, icon: icon) {
|
||||
Text(description)
|
||||
@@ -731,6 +763,23 @@ struct SettingsView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
if let notice {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
Text(notice)
|
||||
.font(.system(size: 12))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.foregroundStyle(AppTheme.warningText)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(AppTheme.warningSurface, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
|
||||
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
||||
infoRow(item.label, item.value, showBorder: true)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useEffect, useRef, useState, type ComponentRef, type ReactNode } from "
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import Constants from "expo-constants";
|
||||
import type { InstanceHealth } from "@edgeever/client";
|
||||
import { buildGitHubFeedbackUrl, type AuthUser } from "@edgeever/shared";
|
||||
import { buildGitHubFeedbackUrl, isClientAheadOfInstance, type AuthUser } from "@edgeever/shared";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { BackHandler, Linking, Modal, Platform, ScrollView, Switch, View } from "react-native";
|
||||
import { Activity, ActivityIndicator, Check, ChevronDown, ChevronLeft, ChevronRight, Cloud, Copy, ExternalLink, Image as ImageIcon, Info, LogOut, MessageSquare, MonitorSmartphone, Moon, RefreshCw, ShieldCheck, SlidersHorizontal, Sun, UserRound } from "../components/icons";
|
||||
import { Activity, ActivityIndicator, AlertTriangle, Check, ChevronDown, ChevronLeft, ChevronRight, Cloud, Copy, ExternalLink, Image as ImageIcon, Info, LogOut, MessageSquare, MonitorSmartphone, Moon, RefreshCw, ShieldCheck, SlidersHorizontal, Sun, UserRound } from "../components/icons";
|
||||
import { Pressable, Text } from "../components/LocalizedText";
|
||||
import { useMobileLocale } from "../lib/mobile-locale";
|
||||
import { useMobileTheme } from "../lib/mobile-theme";
|
||||
@@ -53,6 +53,7 @@ type MobileSystemInfoGroup = {
|
||||
description: string;
|
||||
id: "cloud" | "client" | "connection";
|
||||
items: Array<{ label: string; value: string }>;
|
||||
notice?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
@@ -448,6 +449,12 @@ const MobileSystemInfoSection = ({ group }: { group: MobileSystemInfoGroup }) =>
|
||||
<Text style={styles.systemInfoSectionDescription}>{group.description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{group.notice ? (
|
||||
<View accessibilityLiveRegion="polite" style={styles.systemInfoNotice}>
|
||||
<AlertTriangle color="#d97706" size={14} />
|
||||
<Text style={styles.systemInfoNoticeText}>{group.notice}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.systemInfoRows}>
|
||||
{Array.from({ length: Math.ceil(group.items.length / 3) }, (_, rowIndex) => {
|
||||
const rowItems = group.items.slice(rowIndex * 3, rowIndex * 3 + 3);
|
||||
@@ -479,6 +486,7 @@ const getMobileSystemInfoText = (localePreference: MobileLocaleMode) =>
|
||||
? {
|
||||
build: "Build",
|
||||
client: "Client",
|
||||
clientAheadOfInstance: "This client is newer than the connected cloud instance, so some features may not work yet. You can wait for the daily automatic instance update, or update the instance manually.",
|
||||
clientDescription: "The EdgeEver app and runtime environment on this device.",
|
||||
clientSection: "Current client",
|
||||
cloudDescription: "Version and deployment environment for the connected instance.",
|
||||
@@ -519,6 +527,7 @@ const getMobileSystemInfoText = (localePreference: MobileLocaleMode) =>
|
||||
: {
|
||||
build: "构建",
|
||||
client: "客户端",
|
||||
clientAheadOfInstance: "当前客户端版本高于云端实例,部分功能可能暂不可用。可等待每天自动更新,也可手动更新实例。",
|
||||
clientDescription: "这台设备上的 EdgeEver 应用与运行环境。",
|
||||
clientSection: "当前客户端",
|
||||
cloudDescription: "当前连接实例的版本与部署环境。",
|
||||
@@ -628,6 +637,7 @@ const getMobileSystemInfoGroups = (
|
||||
{
|
||||
description: copy.cloudDescription,
|
||||
id: "cloud",
|
||||
notice: isClientAheadOfInstance(MOBILE_APP_VERSION, instance?.version) ? copy.clientAheadOfInstance : undefined,
|
||||
items: [
|
||||
{ label: copy.instanceVersion, value: instance?.version ? `v${instance.version.replace(/^v/, "")}` : copy.unknown },
|
||||
{ label: copy.instanceBuild, value: instance?.health.build || copy.unknown },
|
||||
|
||||
@@ -1077,6 +1077,23 @@ const baseWorkspaceStyles = StyleSheet.create({
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
},
|
||||
systemInfoNotice: {
|
||||
alignItems: "flex-start",
|
||||
backgroundColor: "#fff7ed",
|
||||
borderColor: "#fed7aa",
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
systemInfoNoticeText: {
|
||||
color: "#9a3412",
|
||||
flex: 1,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
},
|
||||
systemInfoRow: {
|
||||
borderBottomColor: "#e2e8f0",
|
||||
borderBottomWidth: 1,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
const source = readFileSync(new URL("./SystemInfoPanel.tsx", import.meta.url), "utf8");
|
||||
|
||||
describe("system information client/instance version hint", () => {
|
||||
test("warns in system info when the client is ahead of the connected instance", () => {
|
||||
expect(source).toContain("isClientAheadOfInstance");
|
||||
expect(source).toContain("clientRuntimeQuery.data?.appVersion ?? __EDGEEVER_APP_VERSION__");
|
||||
expect(source).toContain("release?.version");
|
||||
expect(source).toContain('t("systemInfo.clientAheadOfInstance")');
|
||||
expect(source).toContain("clientAheadOfInstance");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { DeploymentMetadata } from "@edgeever/shared/deployment-metadata";
|
||||
import { Activity, CircleCheck, Cloud, Copy, ExternalLink, LoaderCircle, MonitorSmartphone, RefreshCw, RotateCcw } from "lucide-react";
|
||||
import { Activity, CircleCheck, Cloud, Copy, ExternalLink, LoaderCircle, MonitorSmartphone, RefreshCw, RotateCcw, TriangleAlert } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice";
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type ClientSyncDiagnostics,
|
||||
} from "@/lib/system-diagnostics";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getReleaseTagForVersion } from "@/lib/version-check";
|
||||
import { getReleaseTagForVersion, isClientAheadOfInstance } from "@/lib/version-check";
|
||||
import { copyTextToClipboard } from "./settings-utils";
|
||||
|
||||
export type SystemInfoItem = {
|
||||
@@ -337,6 +337,10 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
window.setTimeout(() => setCopied(false), 1600);
|
||||
};
|
||||
|
||||
const clientAheadOfInstance = isClientAheadOfInstance(
|
||||
clientRuntimeQuery.data?.appVersion ?? __EDGEEVER_APP_VERSION__,
|
||||
release?.version,
|
||||
);
|
||||
const desktopUpdateState = desktopUpdateStatusQuery.data?.state ?? "idle";
|
||||
const desktopAutoUpdateSupported = clientRuntimeQuery.data?.autoUpdateSupported !== false;
|
||||
const desktopUpdateBusy = desktopUpdateCheckMutation.isPending || desktopUpdateInstallMutation.isPending;
|
||||
@@ -430,7 +434,12 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{isCloud && active && release ? (
|
||||
{isCloud && active && clientAheadOfInstance ? (
|
||||
<p className="flex items-start gap-2 rounded-lg border border-amber-200/80 bg-amber-50/70 px-3 py-1.5 text-xs leading-5 text-amber-900" role="status">
|
||||
<TriangleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600" />
|
||||
<span>{t("systemInfo.clientAheadOfInstance")}</span>
|
||||
</p>
|
||||
) : isCloud && active && release ? (
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-lg border border-emerald-200/80 bg-emerald-50/70 px-3 py-1.5 text-slate-800" role="status">
|
||||
<CircleCheck className="h-3.5 w-3.5 shrink-0 text-emerald-600" />
|
||||
<div className="min-w-0 flex-1 text-xs font-medium text-emerald-950">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { findDesktopReleaseVersion, getReleaseTagForVersion, isVersionOutdated, resolveLocalizedReleaseChanges } from "./version-check";
|
||||
import { findDesktopReleaseVersion, getReleaseTagForVersion, isClientAheadOfInstance, isVersionOutdated, resolveLocalizedReleaseChanges } from "./version-check";
|
||||
|
||||
describe("platform release version checks", () => {
|
||||
test("derives the installed desktop version from the DMG asset", () => {
|
||||
@@ -31,6 +31,13 @@ describe("platform release version checks", () => {
|
||||
expect(isVersionOutdated("1.6.50", "1.6.50-beta.10")).toBe(false);
|
||||
});
|
||||
|
||||
test("treats the client as ahead only when it is newer than a known instance", () => {
|
||||
expect(isClientAheadOfInstance("1.66.0", "1.65.1")).toBe(true);
|
||||
expect(isClientAheadOfInstance("1.65.1+4", "1.65.1")).toBe(false);
|
||||
expect(isClientAheadOfInstance("1.66.0", null)).toBe(false);
|
||||
expect(isClientAheadOfInstance("1.66.0", "local")).toBe(false);
|
||||
});
|
||||
|
||||
test("maps deployed builds back to their formal release tag", () => {
|
||||
expect(getReleaseTagForVersion("1.34.1+18")).toBe("v1.34.1");
|
||||
expect(getReleaseTagForVersion("v1.35.0-beta.2+4")).toBe("v1.35.0-beta.2");
|
||||
|
||||
@@ -16,13 +16,7 @@ export const findDesktopReleaseVersion = (assetNames: string[]) => {
|
||||
: null;
|
||||
};
|
||||
|
||||
const parseVersion = (value: string) => {
|
||||
const match = value.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i);
|
||||
return match ? {
|
||||
core: match.slice(1, 4).map(Number),
|
||||
prerelease: match[4]?.split(".") ?? null,
|
||||
} : null;
|
||||
};
|
||||
export { isClientAheadOfInstance, isVersionOutdated } from "@edgeever/shared";
|
||||
|
||||
export const getReleaseTagForVersion = (version: string) => {
|
||||
const match = version.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+[0-9A-Za-z.-]+)?$/i);
|
||||
@@ -49,30 +43,3 @@ export const resolveLocalizedReleaseChanges = (
|
||||
const fallbackLocale = locales.find((locale) => normalizeLocale(locale) === normalizedFallback);
|
||||
return fallbackLocale ? changes[fallbackLocale] : [];
|
||||
};
|
||||
|
||||
export const isVersionOutdated = (currentVersion: string, latestVersion: string) => {
|
||||
const current = parseVersion(currentVersion);
|
||||
const latest = parseVersion(latestVersion);
|
||||
if (!current || !latest) return false;
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (current.core[index] !== latest.core[index]) return current.core[index] < latest.core[index];
|
||||
}
|
||||
if (!current.prerelease) return false;
|
||||
if (!latest.prerelease) return true;
|
||||
const length = Math.max(current.prerelease.length, latest.prerelease.length);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const currentPart = current.prerelease[index];
|
||||
const latestPart = latest.prerelease[index];
|
||||
if (currentPart === undefined) return true;
|
||||
if (latestPart === undefined) return false;
|
||||
if (currentPart === latestPart) continue;
|
||||
const currentNumber = /^\d+$/.test(currentPart) ? Number(currentPart) : null;
|
||||
const latestNumber = /^\d+$/.test(latestPart) ? Number(latestPart) : null;
|
||||
if (currentNumber !== null && latestNumber !== null) return currentNumber < latestNumber;
|
||||
if (currentNumber !== null) return true;
|
||||
if (latestNumber !== null) return false;
|
||||
return currentPart.localeCompare(latestPart) < 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
+1
-1
@@ -63,7 +63,7 @@
|
||||
"release": "bun scripts/release.mjs",
|
||||
"publish:stores": "bun scripts/store-delivery.mjs",
|
||||
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
|
||||
"test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/instance-update-gate.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
|
||||
"test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
|
||||
"verify:desktop-renderer": "bun scripts/verify-desktop-renderer.mjs",
|
||||
"verify:renderer-origin-migration": "bun run --cwd apps/desktop verify-origin-migration",
|
||||
"verify:desktop-cross-version-startup": "bun scripts/verify-desktop-cross-version-startup.mjs",
|
||||
|
||||
@@ -1137,6 +1137,7 @@ export const enUS = {
|
||||
browserMode: "Browser",
|
||||
unknown: "Unknown",
|
||||
copy: "Copy info",
|
||||
clientAheadOfInstance: "This client is newer than the connected cloud instance, so some features may not work yet. You can wait for the daily automatic instance update, or update the instance manually.",
|
||||
deployedUpdateTitle: "Your cloud instance was automatically updated to v{{version}}",
|
||||
viewReleaseNotes: "Release notes",
|
||||
desktopCheckForUpdates: "Check for updates",
|
||||
|
||||
@@ -1137,6 +1137,7 @@ export const zhCN = {
|
||||
browserMode: "浏览器",
|
||||
unknown: "未知",
|
||||
copy: "复制信息",
|
||||
clientAheadOfInstance: "当前客户端版本高于云端实例,部分功能可能暂不可用。可等待每天自动更新,也可手动更新实例。",
|
||||
deployedUpdateTitle: "您的云端实例已自动更新至 v{{version}}",
|
||||
viewReleaseNotes: "版本说明",
|
||||
desktopCheckForUpdates: "检查更新",
|
||||
|
||||
@@ -39,6 +39,7 @@ export * from "./file-attachment";
|
||||
export * from "./note-image-card";
|
||||
export * from "./pdf-attachment";
|
||||
export * from "./types";
|
||||
export * from "./version";
|
||||
export * from './plugin-capabilities';
|
||||
|
||||
export {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isClientAheadOfInstance, isVersionOutdated } from "./version";
|
||||
|
||||
describe("release version comparison", () => {
|
||||
test("compares semantic release components and ignores build metadata", () => {
|
||||
expect(isVersionOutdated("1.6.49+12", "1.6.50")).toBe(true);
|
||||
expect(isVersionOutdated("1.6.50+3", "1.6.50")).toBe(false);
|
||||
expect(isVersionOutdated("1.6.50-beta.2", "1.6.50-beta.10")).toBe(true);
|
||||
expect(isVersionOutdated("1.6.50-beta.10", "1.6.50")).toBe(true);
|
||||
expect(isVersionOutdated("1.6.50", "1.6.50-beta.10")).toBe(false);
|
||||
});
|
||||
|
||||
test("treats the client as ahead only when it is newer than a known instance", () => {
|
||||
expect(isClientAheadOfInstance("1.66.0", "1.65.1")).toBe(true);
|
||||
expect(isClientAheadOfInstance("v1.66.0", "v1.65.1")).toBe(true);
|
||||
expect(isClientAheadOfInstance("1.65.1+4", "1.65.1")).toBe(false);
|
||||
expect(isClientAheadOfInstance("1.65.1", "1.65.1")).toBe(false);
|
||||
expect(isClientAheadOfInstance("1.64.0", "1.65.1")).toBe(false);
|
||||
expect(isClientAheadOfInstance("1.66.0", null)).toBe(false);
|
||||
expect(isClientAheadOfInstance("1.66.0", "")).toBe(false);
|
||||
expect(isClientAheadOfInstance("1.66.0", "local")).toBe(false);
|
||||
expect(isClientAheadOfInstance(null, "1.65.1")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,16 @@
|
||||
const parseVersion = (value) => {
|
||||
const match = String(value ?? "").match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i);
|
||||
return match
|
||||
? { core: match.slice(1, 4).map(Number), prerelease: match[4]?.split(".") ?? null }
|
||||
: null;
|
||||
const parseVersion = (value: string) => {
|
||||
const match = value.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i);
|
||||
return match ? {
|
||||
core: match.slice(1, 4).map(Number),
|
||||
prerelease: match[4]?.split(".") ?? null,
|
||||
} : null;
|
||||
};
|
||||
|
||||
const isVersionOutdated = (currentVersion, latestVersion) => {
|
||||
export const isVersionOutdated = (currentVersion: string, latestVersion: string) => {
|
||||
const current = parseVersion(currentVersion);
|
||||
const latest = parseVersion(latestVersion);
|
||||
if (!current || !latest) return false;
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (current.core[index] !== latest.core[index]) return current.core[index] < latest.core[index];
|
||||
}
|
||||
@@ -31,12 +33,7 @@ const isVersionOutdated = (currentVersion, latestVersion) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const instanceReleaseVersionFromPayload = (payload) => (
|
||||
payload && typeof payload.version === "string" && payload.version.trim()
|
||||
? payload.version.trim()
|
||||
: null
|
||||
);
|
||||
|
||||
export const shouldHoldAutoRestartUpdate = (updateVersion, instanceVersion) => (
|
||||
Boolean(updateVersion && instanceVersion && isVersionOutdated(instanceVersion, updateVersion))
|
||||
);
|
||||
export const isClientAheadOfInstance = (
|
||||
clientVersion: string | null | undefined,
|
||||
instanceVersion: string | null | undefined,
|
||||
) => Boolean(clientVersion && instanceVersion && isVersionOutdated(instanceVersion, clientVersion));
|
||||
Reference in new issue
Block a user