From 1486a4a2b9c3f5a83d54f5ce591a2f60fba9316f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 11:20:46 -0700 Subject: [PATCH] [codex] Structure Electron updater errors (#3280) Co-authored-by: codex --- .../src/electron/ElectronUpdater.test.ts | 66 ++++++++++++++++--- apps/desktop/src/electron/ElectronUpdater.ts | 58 ++++++++++------ 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/electron/ElectronUpdater.test.ts b/apps/desktop/src/electron/ElectronUpdater.test.ts index 43a3c84dcd4a03c4549ca0ee67a720d5dec19ea1..8fcc34f41c24e74a6165cb7495f6475af7a769d6 100644 --- a/apps/desktop/src/electron/ElectronUpdater.test.ts +++ b/apps/desktop/src/electron/ElectronUpdater.test.ts @@ -1,5 +1,4 @@ import { assert, describe, it } from "@effect/vitest"; -import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import { beforeEach, vi } from "vite-plus/test"; @@ -65,16 +64,65 @@ describe("ElectronUpdater", () => { const cause = new Error("network unavailable"); autoUpdaterMock.checkForUpdates.mockImplementationOnce(() => Promise.reject(cause)); const updater = yield* ElectronUpdater.ElectronUpdater; + autoUpdaterMock.channel = "beta"; - const exit = yield* Effect.exit(updater.checkForUpdates); + const error = yield* updater.checkForUpdates.pipe(Effect.flip); - assert.equal(exit._tag, "Failure"); - if (exit._tag === "Failure") { - const error = Cause.squash(exit.cause); - assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError); - assert.equal(error.cause, cause); - assert.equal(error.message, "Electron updater failed to check for updates."); - } + assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError); + assert.isTrue(ElectronUpdater.isElectronUpdaterError(error)); + assert.equal(error.channel, "beta"); + assert.strictEqual(error.cause, cause); + assert.equal(error.message, "Electron updater failed to check for updates on channel beta."); + assert.notInclude(error.message, cause.message); + }).pipe(Effect.provide(ElectronUpdater.layer)), + ); + + it.effect("preserves the execution-time channel on download failures", () => + Effect.gen(function* () { + const cause = new Error("download unavailable"); + autoUpdaterMock.downloadUpdate.mockImplementationOnce(() => Promise.reject(cause)); + const updater = yield* ElectronUpdater.ElectronUpdater; + autoUpdaterMock.channel = "nightly"; + + const error = yield* updater.downloadUpdate.pipe(Effect.flip); + + assert.instanceOf(error, ElectronUpdater.ElectronUpdaterDownloadUpdateError); + assert.isTrue(ElectronUpdater.isElectronUpdaterError(error)); + assert.equal(error.channel, "nightly"); + assert.strictEqual(error.cause, cause); + assert.equal( + error.message, + "Electron updater failed to download the update on channel nightly.", + ); + assert.notInclude(error.message, cause.message); + }).pipe(Effect.provide(ElectronUpdater.layer)), + ); + + it.effect("preserves quit-and-install flags and the execution-time channel", () => + Effect.gen(function* () { + const cause = new Error("quit and install failed"); + autoUpdaterMock.quitAndInstall.mockImplementationOnce(() => { + throw cause; + }); + const updater = yield* ElectronUpdater.ElectronUpdater; + autoUpdaterMock.channel = "alpha"; + + const error = yield* updater + .quitAndInstall({ isSilent: true, isForceRunAfter: false }) + .pipe(Effect.flip); + + assert.instanceOf(error, ElectronUpdater.ElectronUpdaterQuitAndInstallError); + assert.isTrue(ElectronUpdater.isElectronUpdaterError(error)); + assert.equal(error.channel, "alpha"); + assert.equal(error.isSilent, true); + assert.equal(error.isForceRunAfter, false); + assert.strictEqual(error.cause, cause); + assert.equal( + error.message, + "Electron updater failed to quit and install the update on channel alpha (silent: true, force run after: false).", + ); + assert.notInclude(error.message, cause.message); + assert.deepEqual(autoUpdaterMock.quitAndInstall.mock.calls, [[true, false]]); }).pipe(Effect.provide(ElectronUpdater.layer)), ); }); diff --git a/apps/desktop/src/electron/ElectronUpdater.ts b/apps/desktop/src/electron/ElectronUpdater.ts index 8a468a15c20e53ff83f3caeed230cadcc175457e..435fbd002289702d306353ce76a9a2d4a6cde6ce 100644 --- a/apps/desktop/src/electron/ElectronUpdater.ts +++ b/apps/desktop/src/electron/ElectronUpdater.ts @@ -10,40 +10,41 @@ type AutoUpdater = typeof autoUpdater; export type ElectronUpdaterFeedUrl = Parameters[0]; -const electronUpdaterErrorFields = { - cause: Schema.Defect(), -}; - export class ElectronUpdaterCheckForUpdatesError extends Schema.TaggedErrorClass()( "ElectronUpdaterCheckForUpdatesError", { - ...electronUpdaterErrorFields, + channel: Schema.NullOr(Schema.String), + cause: Schema.Defect(), }, ) { override get message(): string { - return "Electron updater failed to check for updates."; + return `Electron updater failed to check for updates on channel ${this.channel ?? "default"}.`; } } export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedErrorClass()( "ElectronUpdaterDownloadUpdateError", { - ...electronUpdaterErrorFields, + channel: Schema.NullOr(Schema.String), + cause: Schema.Defect(), }, ) { override get message(): string { - return "Electron updater failed to download the update."; + return `Electron updater failed to download the update on channel ${this.channel ?? "default"}.`; } } export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedErrorClass()( "ElectronUpdaterQuitAndInstallError", { - ...electronUpdaterErrorFields, + channel: Schema.NullOr(Schema.String), + isSilent: Schema.Boolean, + isForceRunAfter: Schema.Boolean, + cause: Schema.Defect(), }, ) { override get message(): string { - return "Electron updater failed to quit and install the update."; + return `Electron updater failed to quit and install the update on channel ${this.channel ?? "default"} (silent: ${this.isSilent}, force run after: ${this.isForceRunAfter}).`; } } @@ -116,18 +117,33 @@ export const make = ElectronUpdater.of({ autoUpdater.disableDifferentialDownload = value; return Effect.void; }), - checkForUpdates: Effect.tryPromise({ - try: () => autoUpdater.checkForUpdates(), - catch: (cause) => new ElectronUpdaterCheckForUpdatesError({ cause }), - }).pipe(Effect.asVoid), - downloadUpdate: Effect.tryPromise({ - try: () => autoUpdater.downloadUpdate(), - catch: (cause) => new ElectronUpdaterDownloadUpdateError({ cause }), - }).pipe(Effect.asVoid), + checkForUpdates: Effect.suspend(() => { + const channel = autoUpdater.channel; + return Effect.tryPromise({ + try: () => autoUpdater.checkForUpdates(), + catch: (cause) => new ElectronUpdaterCheckForUpdatesError({ channel, cause }), + }).pipe(Effect.asVoid); + }), + downloadUpdate: Effect.suspend(() => { + const channel = autoUpdater.channel; + return Effect.tryPromise({ + try: () => autoUpdater.downloadUpdate(), + catch: (cause) => new ElectronUpdaterDownloadUpdateError({ channel, cause }), + }).pipe(Effect.asVoid); + }), quitAndInstall: ({ isSilent, isForceRunAfter }) => - Effect.try({ - try: () => autoUpdater.quitAndInstall(isSilent, isForceRunAfter), - catch: (cause) => new ElectronUpdaterQuitAndInstallError({ cause }), + Effect.suspend(() => { + const channel = autoUpdater.channel; + return Effect.try({ + try: () => autoUpdater.quitAndInstall(isSilent, isForceRunAfter), + catch: (cause) => + new ElectronUpdaterQuitAndInstallError({ + channel, + isSilent, + isForceRunAfter, + cause, + }), + }); }), on: (eventName, listener) => { const eventTarget = autoUpdater as unknown as { -- 2.54.0