Show found desktop updates after manual checks (#1815)

* fix(desktop): report found app updates during manual checks

Manual checks could reuse cached update state while the renderer discarded pending update details. Keep found update versions visible while downloads prepare, and reserve install affordances for ready updates.

* test(desktop): cover manual update retry after errors

* fix(desktop): keep ready updates ready on recheck
This commit is contained in:
Mohamed Boudra
2026-06-30 08:12:15 +02:00
committed by GitHub
parent 5fc53c576e
commit 7c6152663e
16 changed files with 209 additions and 16 deletions
@@ -58,6 +58,10 @@ class FakeAppUpdateRuntime implements AppUpdateRuntime {
this.configuration?.onUpdateAvailable(info);
}
finishUpdateDownload(info: RuntimeUpdateInfo): void {
this.configuration?.onUpdateDownloaded(info);
}
async checkForUpdates(): Promise<{
isUpdateAvailable: boolean;
updateInfo: RuntimeUpdateInfo;
@@ -144,6 +148,37 @@ describe("app update service", () => {
});
});
it("performs a fresh manual check when an update is already cached", async () => {
const { runtime, service } = createService({ bucket: async () => 0 });
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
await service.checkForAppUpdate({
currentVersion: "1.2.3",
releaseChannel: "stable",
intent: "automatic",
});
runtime.nextCheck({
isUpdateAvailable: true,
updateInfo: { ...rolledOutUpdate, version: "1.2.5" },
});
const result = await service.checkForAppUpdate({
currentVersion: "1.2.3",
releaseChannel: "stable",
intent: "manual",
});
expect(result).toEqual({
hasUpdate: true,
readyToInstall: false,
currentVersion: "1.2.3",
latestVersion: "1.2.5",
body: null,
date: "2026-04-28T00:00:00.000Z",
errorMessage: null,
});
});
it("trusts the runtime availability decision before comparing versions", async () => {
const { runtime, service } = createService({ bucket: async () => 0 });
runtime.nextCheck({ isUpdateAvailable: false, updateInfo: rolledOutUpdate });
@@ -347,6 +382,70 @@ describe("app update service", () => {
});
});
it("performs a fresh manual check after an update preparation error", async () => {
const { runtime, service } = createService();
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
await service.checkForAppUpdate({
currentVersion: "1.2.3",
releaseChannel: "stable",
intent: "manual",
});
runtime.failRuntime(new Error("sha512 checksum mismatch"));
runtime.nextCheck({
isUpdateAvailable: true,
updateInfo: { ...rolledOutUpdate, version: "1.2.5" },
});
const result = await service.checkForAppUpdate({
currentVersion: "1.2.3",
releaseChannel: "stable",
intent: "manual",
});
expect(result).toEqual({
hasUpdate: true,
readyToInstall: false,
currentVersion: "1.2.3",
latestVersion: "1.2.5",
body: null,
date: "2026-04-28T00:00:00.000Z",
errorMessage: null,
});
});
it("keeps a downloaded update ready when a manual check re-announces 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);
const recheck = runtime.deferNextCheck();
const pending = service.checkForAppUpdate({
currentVersion: "1.2.3",
releaseChannel: "stable",
intent: "manual",
});
runtime.prepareUpdate(rolledOutUpdate);
recheck.resolve({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
const result = await pending;
expect(result).toEqual({
hasUpdate: true,
readyToInstall: true,
currentVersion: "1.2.3",
latestVersion: "1.2.4",
body: null,
date: "2026-04-28T00:00:00.000Z",
errorMessage: null,
});
});
it("returns runtime update errors to multiple automatic checks before a manual retry clears them", async () => {
const { runtime, service } = createService();
runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate });
@@ -163,9 +163,10 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
});
},
onUpdateAvailable(info) {
const alreadyReady = downloadedUpdateVersion === info.version;
cachedUpdateInfo = info;
downloadedUpdateVersion = null;
downloading = true;
downloadedUpdateVersion = alreadyReady ? info.version : null;
downloading = !alreadyReady;
runtimeErrorMessage = null;
},
onUpdateDownloaded(info) {
@@ -212,7 +213,12 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
}
const cachedVersion = cachedUpdateInfo?.version ?? null;
if (!runtimeErrorResult && cachedVersion && cachedVersion !== currentVersion) {
if (
!runtimeErrorResult &&
intent === "automatic" &&
cachedVersion &&
cachedVersion !== currentVersion
) {
return buildCheckResult({
currentVersion,
hasUpdate: true,