From 7a4c9bd101c2773184ac2137d87fd4ad05d77ccd Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Tue, 28 Apr 2026 13:37:43 -0700 Subject: [PATCH] feat(code): do not check for updates after update restart --- .../src/main/services/updates/service.test.ts | 76 +++++++++++++++++++ .../code/src/main/services/updates/service.ts | 24 +++++- apps/code/src/main/utils/store.ts | 12 +++ 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/apps/code/src/main/services/updates/service.test.ts b/apps/code/src/main/services/updates/service.test.ts index 7a07a88ce..e4190bd37 100644 --- a/apps/code/src/main/services/updates/service.test.ts +++ b/apps/code/src/main/services/updates/service.test.ts @@ -8,6 +8,7 @@ const { mockAppMeta, mockMainWindow, mockLifecycleService, + mockUpdatesStore, updaterHandlers, } = vi.hoisted(() => { const updaterHandlers: { @@ -80,6 +81,13 @@ const { shutdownWithoutContainer: vi.fn(() => Promise.resolve()), setQuittingForUpdate: vi.fn(), }, + mockUpdatesStore: { + _value: null as string | null, + get: vi.fn((_key: string) => mockUpdatesStore._value), + set: vi.fn((_key: string, value: string) => { + mockUpdatesStore._value = value; + }), + }, }; }); @@ -98,6 +106,10 @@ vi.mock("../../utils/env.js", () => ({ isDevBuild: () => !mockAppMeta.isProduction, })); +vi.mock("../../utils/store.js", () => ({ + updatesStore: mockUpdatesStore, +})); + // Import the service after mocks are set up import { UpdatesService } from "./service"; @@ -145,6 +157,9 @@ describe("UpdatesService", () => { // Clear env flag delete process.env.ELECTRON_DISABLE_AUTO_UPDATE; + // Reset persisted updates store + mockUpdatesStore._value = null; + service = new UpdatesService(); injectPorts(service); }); @@ -270,6 +285,67 @@ describe("UpdatesService", () => { }); }); + describe("initial check on startup", () => { + it.each([ + { + desc: "runs initial check on first ever launch (no stored version)", + storedVersion: null, + appVersion: "1.0.0", + expectCheck: true, + expectSet: "1.0.0" as string | null, + }, + { + desc: "runs initial check when relaunching the same version", + storedVersion: "1.0.0", + appVersion: "1.0.0", + expectCheck: true, + expectSet: null, + }, + { + desc: "skips initial check after a version change (post-update restart)", + storedVersion: "1.0.0", + appVersion: "1.0.1", + expectCheck: false, + expectSet: "1.0.1" as string | null, + }, + ])( + "$desc", + async ({ storedVersion, appVersion, expectCheck, expectSet }) => { + mockUpdatesStore._value = storedVersion; + mockAppMeta.version = appVersion; + + await initializeService(service); + + if (expectCheck) { + expect(mockUpdater.check).toHaveBeenCalled(); + } else { + expect(mockUpdater.check).not.toHaveBeenCalled(); + } + + if (expectSet !== null) { + expect(mockUpdatesStore.set).toHaveBeenCalledWith( + "lastLaunchedVersion", + expectSet, + ); + } else { + expect(mockUpdatesStore.set).not.toHaveBeenCalled(); + } + }, + ); + + it("still schedules the periodic check after skipping the initial one", async () => { + mockUpdatesStore._value = "1.0.0"; + mockAppMeta.version = "1.0.1"; + + await initializeService(service); + expect(mockUpdater.check).not.toHaveBeenCalled(); + + // Advance past the 24h interval + await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000); + expect(mockUpdater.check).toHaveBeenCalled(); + }); + }); + describe("feedUrl", () => { it("constructs correct feed URL with platform, arch, and version", async () => { Object.defineProperty(process, "arch", { diff --git a/apps/code/src/main/services/updates/service.ts b/apps/code/src/main/services/updates/service.ts index ecfbac89f..ba53a4e62 100644 --- a/apps/code/src/main/services/updates/service.ts +++ b/apps/code/src/main/services/updates/service.ts @@ -6,6 +6,7 @@ import { inject, injectable, postConstruct, preDestroy } from "inversify"; import { MAIN_TOKENS } from "../../di/tokens"; import { isDevBuild } from "../../utils/env"; import { logger } from "../../utils/logger"; +import { updatesStore } from "../../utils/store"; import { TypedEventEmitter } from "../../utils/typed-event-emitter"; import type { AppLifecycleService } from "../app-lifecycle/service"; import { @@ -191,8 +192,27 @@ export class UpdatesService extends TypedEventEmitter { ), ); - // Perform initial check (periodic source — not user-initiated) - this.checkForUpdates("periodic"); + // Skip the initial check if the app version changed since last launch — + // that means we just restarted to apply an update, and re-checking + // immediately tends to find another release that shipped in the meantime. + const lastLaunchedVersion = updatesStore.get("lastLaunchedVersion"); + const currentVersion = this.appMeta.version; + const isPostUpdateRestart = + lastLaunchedVersion !== null && lastLaunchedVersion !== currentVersion; + + if (isPostUpdateRestart) { + log.info("Skipping initial update check after version change", { + previousVersion: lastLaunchedVersion, + currentVersion, + }); + } else { + // Perform initial check (periodic source — not user-initiated) + this.checkForUpdates("periodic"); + } + + if (lastLaunchedVersion !== currentVersion) { + updatesStore.set("lastLaunchedVersion", currentVersion); + } // Set up periodic checks this.checkIntervalId = setInterval( diff --git a/apps/code/src/main/utils/store.ts b/apps/code/src/main/utils/store.ts index 4f511563e..796bde2f2 100644 --- a/apps/code/src/main/utils/store.ts +++ b/apps/code/src/main/utils/store.ts @@ -26,6 +26,10 @@ export interface WindowStateSchema { isMaximized: boolean; } +interface UpdatesStoreSchema { + lastLaunchedVersion: string | null; +} + const userDataDir = getUserDataDir(); export const rendererStore = new Store({ @@ -52,3 +56,11 @@ export const windowStateStore = new Store({ isMaximized: true, }, }); + +export const updatesStore = new Store({ + name: "updates", + cwd: userDataDir, + defaults: { + lastLaunchedVersion: null, + }, +});