mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
fix(desktop): keep manually admitted updates available (#3865)
Automatic readiness polls reapplied the stable rollout gate and cleared the same update that a manual check had already admitted. Keep same-version admission monotonic while still clearing withdrawn releases and rejecting newer releases that have not reached the user.
This commit is contained in:
@@ -58,6 +58,7 @@ export interface DesktopRuntimeConfig {
|
||||
updateAvailable?: boolean;
|
||||
latestVersion?: string;
|
||||
updateReadyToInstall?: boolean;
|
||||
manualUpdateBypassesRollout?: boolean;
|
||||
slowInstall?: boolean;
|
||||
/** Initial PID reported by desktop_daemon_status. Defaults to null. */
|
||||
daemonPid?: number | null;
|
||||
@@ -139,6 +140,7 @@ export async function installDesktopRuntime(
|
||||
let daemonRunning = true;
|
||||
let currentPid: number | null = cfg.daemonPid ?? null;
|
||||
let startCount = 0;
|
||||
let manualUpdateAdmitted = false;
|
||||
window.__desktopDaemonStartRequested = false;
|
||||
|
||||
function buildDaemonStatus() {
|
||||
@@ -175,6 +177,33 @@ export async function installDesktopRuntime(
|
||||
}
|
||||
}
|
||||
|
||||
function buildAppUpdateCheckResult(hasUpdate: boolean, readyToInstall: boolean) {
|
||||
return {
|
||||
hasUpdate,
|
||||
readyToInstall,
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: hasUpdate ? (cfg.latestVersion ?? "1.2.3") : null,
|
||||
body: null,
|
||||
date: null,
|
||||
};
|
||||
}
|
||||
|
||||
function checkAppUpdate(intent: unknown) {
|
||||
if (!cfg.manualUpdateBypassesRollout) {
|
||||
return buildAppUpdateCheckResult(
|
||||
cfg.updateAvailable === true,
|
||||
cfg.updateAvailable === true && (cfg.updateReadyToInstall ?? true),
|
||||
);
|
||||
}
|
||||
|
||||
if (intent === "manual") {
|
||||
manualUpdateAdmitted = true;
|
||||
return buildAppUpdateCheckResult(true, false);
|
||||
}
|
||||
|
||||
return buildAppUpdateCheckResult(manualUpdateAdmitted, manualUpdateAdmitted);
|
||||
}
|
||||
|
||||
const desktopBridge: {
|
||||
platform: string;
|
||||
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
@@ -192,23 +221,7 @@ export async function installDesktopRuntime(
|
||||
platform: "darwin",
|
||||
invoke: async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === "check_app_update") {
|
||||
return cfg.updateAvailable
|
||||
? {
|
||||
hasUpdate: true,
|
||||
readyToInstall: cfg.updateReadyToInstall ?? true,
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: cfg.latestVersion ?? "1.2.3",
|
||||
body: null,
|
||||
date: null,
|
||||
}
|
||||
: {
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: null,
|
||||
body: null,
|
||||
date: null,
|
||||
};
|
||||
return checkAppUpdate(args?.intent);
|
||||
}
|
||||
|
||||
if (command === "install_app_update") {
|
||||
@@ -353,6 +366,14 @@ export async function expectPendingUpdateCheckResult(page: Page, version: string
|
||||
await expect(page.getByRole("button", { name: "Update" })).toBeDisabled();
|
||||
}
|
||||
|
||||
export async function expectReadyUpdateCheckResult(page: Page, version: string): Promise<void> {
|
||||
const normalizedVersion = `v${version.replace(/^v/i, "")}`;
|
||||
await expect(page.getByText(`Ready to install: ${normalizedVersion}`)).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.getByRole("button", { name: `Update to ${normalizedVersion}` })).toBeEnabled();
|
||||
}
|
||||
|
||||
export async function clickInstallUpdate(page: Page): Promise<void> {
|
||||
await page.getByRole("button", { name: "Install & restart" }).click();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
expectUpdateBanner,
|
||||
clickCheckForUpdates,
|
||||
expectPendingUpdateCheckResult,
|
||||
expectReadyUpdateCheckResult,
|
||||
clickInstallUpdate,
|
||||
expectInstallInProgress,
|
||||
interceptDaemonManagementConfirmDialog,
|
||||
@@ -86,6 +87,20 @@ test.describe("Desktop updates", () => {
|
||||
|
||||
await expectPendingUpdateCheckResult(page, "1.2.3");
|
||||
});
|
||||
|
||||
test("manual update remains available after the automatic rollout recheck", async ({ page }) => {
|
||||
await installDesktopRuntime(page, {
|
||||
serverId: getServerId(),
|
||||
latestVersion: "1.2.3",
|
||||
manualUpdateBypassesRollout: true,
|
||||
});
|
||||
await gotoAppShell(page);
|
||||
await openDesktopAboutSettings(page);
|
||||
|
||||
await clickCheckForUpdates(page);
|
||||
await expectPendingUpdateCheckResult(page, "1.2.3");
|
||||
await expectReadyUpdateCheckResult(page, "1.2.3");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Desktop daemon management", () => {
|
||||
|
||||
@@ -208,6 +208,76 @@ describe("app update service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a manually admitted update after a rollout-gated automatic recheck", async () => {
|
||||
const { runtime, service } = createService();
|
||||
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
|
||||
|
||||
await service.checkForAppUpdate({
|
||||
currentVersion: "1.2.3",
|
||||
releaseChannel: "stable",
|
||||
intent: "manual",
|
||||
});
|
||||
runtime.finishUpdateDownload(rolledOutUpdate);
|
||||
|
||||
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
|
||||
const result = await service.checkForAppUpdate({
|
||||
currentVersion: "1.2.3",
|
||||
releaseChannel: "stable",
|
||||
intent: "automatic",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
hasUpdate: true,
|
||||
readyToInstall: true,
|
||||
latestVersion: "1.2.4",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps preparing a manually admitted update after a rollout-gated automatic recheck", async () => {
|
||||
const { runtime, service } = createService();
|
||||
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
|
||||
|
||||
await service.checkForAppUpdate({
|
||||
currentVersion: "1.2.3",
|
||||
releaseChannel: "stable",
|
||||
intent: "manual",
|
||||
});
|
||||
|
||||
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
|
||||
const result = await service.checkForAppUpdate({
|
||||
currentVersion: "1.2.3",
|
||||
releaseChannel: "stable",
|
||||
intent: "automatic",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
hasUpdate: true,
|
||||
readyToInstall: false,
|
||||
latestVersion: "1.2.4",
|
||||
});
|
||||
});
|
||||
|
||||
it("clears a cached update when the manifest no longer contains it", async () => {
|
||||
const { runtime, service } = createService();
|
||||
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
|
||||
|
||||
await service.checkForAppUpdate({
|
||||
currentVersion: "1.2.3",
|
||||
releaseChannel: "stable",
|
||||
intent: "manual",
|
||||
});
|
||||
runtime.finishUpdateDownload(rolledOutUpdate);
|
||||
|
||||
runtime.nextCheck(null);
|
||||
const result = await service.checkForAppUpdate({
|
||||
currentVersion: "1.2.3",
|
||||
releaseChannel: "stable",
|
||||
intent: "automatic",
|
||||
});
|
||||
|
||||
expect(result.hasUpdate).toBe(false);
|
||||
});
|
||||
|
||||
it("waits for an automatic poll before starting a manual rollout-bypassing check", async () => {
|
||||
const { runtime, service } = createService();
|
||||
const automaticCheck = runtime.deferNextCheck();
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface AppUpdateRuntimeConfiguration {
|
||||
shouldAdmitUpdate(info: RuntimeUpdateInfo): boolean | Promise<boolean>;
|
||||
onUpdateAvailable(info: RuntimeUpdateInfo): void;
|
||||
onUpdateDownloaded(info: RuntimeUpdateInfo): void;
|
||||
onUpdateNotAvailable(): void;
|
||||
onError(error: unknown): void;
|
||||
}
|
||||
|
||||
@@ -147,6 +146,24 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
|
||||
preparingUpdateVersion = null;
|
||||
}
|
||||
|
||||
function buildPreviouslyAdmittedUpdateResult(
|
||||
currentVersion: string,
|
||||
checkedInfo: RuntimeUpdateInfo,
|
||||
): AppUpdateCheckResult | null {
|
||||
const info = cachedUpdateInfo;
|
||||
if (!info || info.version === currentVersion || info.version !== checkedInfo.version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
hasUpdate: true,
|
||||
readyToInstall: isReadyToInstallVersion(info.version),
|
||||
info,
|
||||
errorMessage: preparationError?.version === info.version ? preparationError.message : null,
|
||||
});
|
||||
}
|
||||
|
||||
function configureRuntime(releaseChannel: AppReleaseChannel, intent: AppUpdateCheckIntent): void {
|
||||
if (configuredReleaseChannel !== releaseChannel) {
|
||||
clearUpdateState();
|
||||
@@ -186,9 +203,6 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
|
||||
preparationError = null;
|
||||
}
|
||||
},
|
||||
onUpdateNotAvailable() {
|
||||
clearUpdateState();
|
||||
},
|
||||
onError(error) {
|
||||
if (preparingUpdateVersion) {
|
||||
preparationError = {
|
||||
@@ -233,7 +247,24 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
|
||||
|
||||
try {
|
||||
const result = await deps.runtime.checkForUpdates();
|
||||
if (!result || !result.updateInfo || !result.isUpdateAvailable) {
|
||||
if (!result || !result.updateInfo) {
|
||||
clearUpdateState();
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!result.isUpdateAvailable) {
|
||||
const admittedUpdate = buildPreviouslyAdmittedUpdateResult(
|
||||
currentVersion,
|
||||
result.updateInfo,
|
||||
);
|
||||
if (admittedUpdate) {
|
||||
return admittedUpdate;
|
||||
}
|
||||
|
||||
clearUpdateState();
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
|
||||
@@ -138,9 +138,6 @@ class ElectronAppUpdateRuntime implements AppUpdateRuntime {
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
input.onUpdateDownloaded(info as RuntimeUpdateInfo);
|
||||
});
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
input.onUpdateNotAvailable();
|
||||
});
|
||||
autoUpdater.on("error", (error) => {
|
||||
if (isUpdateChannelNotPublished(error)) return;
|
||||
input.onError(error);
|
||||
|
||||
Reference in New Issue
Block a user