| author | |
| committer | |
| log | 1486a4a2b9c3f5a83d54f5ce591a2f60fba9316f |
| tree | bad0a2d71476a6beb9eca68e1af5b2df4841328d |
| parent | 1e5f62801b7c4d247247bc3648310a20560e5b43 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>2 files changed, 94 insertions(+), 30 deletions(-)
apps/desktop/src/electron/ElectronUpdater.test.ts+57-9| ... | ... | @@ -1,5 +1,4 @@ |
| 1 | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as Cause from "effect/Cause"; | |
| 3 | 2 | import * as Effect from "effect/Effect"; |
| 4 | 3 | import { beforeEach, vi } from "vite-plus/test"; |
| 5 | 4 | |
| ... | ... | @@ -65,16 +64,65 @@ describe("ElectronUpdater", () => { |
| 65 | 64 | const cause = new Error("network unavailable"); |
| 66 | 65 | autoUpdaterMock.checkForUpdates.mockImplementationOnce(() => Promise.reject(cause)); |
| 67 | 66 | const updater = yield* ElectronUpdater.ElectronUpdater; |
| 67 | autoUpdaterMock.channel = "beta"; | |
| 68 | 68 | |
| 69 | const exit = yield* Effect.exit(updater.checkForUpdates); | |
| 69 | const error = yield* updater.checkForUpdates.pipe(Effect.flip); | |
| 70 | 70 | |
| 71 | assert.equal(exit._tag, "Failure"); | |
| 72 | if (exit._tag === "Failure") { | |
| 73 | const error = Cause.squash(exit.cause); | |
| 74 | assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError); | |
| 75 | assert.equal(error.cause, cause); | |
| 76 | assert.equal(error.message, "Electron updater failed to check for updates."); | |
| 77 | } | |
| 71 | assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError); | |
| 72 | assert.isTrue(ElectronUpdater.isElectronUpdaterError(error)); | |
| 73 | assert.equal(error.channel, "beta"); | |
| 74 | assert.strictEqual(error.cause, cause); | |
| 75 | assert.equal(error.message, "Electron updater failed to check for updates on channel beta."); | |
| 76 | assert.notInclude(error.message, cause.message); | |
| 77 | }).pipe(Effect.provide(ElectronUpdater.layer)), | |
| 78 | ); | |
| 79 | ||
| 80 | it.effect("preserves the execution-time channel on download failures", () => | |
| 81 | Effect.gen(function* () { | |
| 82 | const cause = new Error("download unavailable"); | |
| 83 | autoUpdaterMock.downloadUpdate.mockImplementationOnce(() => Promise.reject(cause)); | |
| 84 | const updater = yield* ElectronUpdater.ElectronUpdater; | |
| 85 | autoUpdaterMock.channel = "nightly"; | |
| 86 | ||
| 87 | const error = yield* updater.downloadUpdate.pipe(Effect.flip); | |
| 88 | ||
| 89 | assert.instanceOf(error, ElectronUpdater.ElectronUpdaterDownloadUpdateError); | |
| 90 | assert.isTrue(ElectronUpdater.isElectronUpdaterError(error)); | |
| 91 | assert.equal(error.channel, "nightly"); | |
| 92 | assert.strictEqual(error.cause, cause); | |
| 93 | assert.equal( | |
| 94 | error.message, | |
| 95 | "Electron updater failed to download the update on channel nightly.", | |
| 96 | ); | |
| 97 | assert.notInclude(error.message, cause.message); | |
| 98 | }).pipe(Effect.provide(ElectronUpdater.layer)), | |
| 99 | ); | |
| 100 | ||
| 101 | it.effect("preserves quit-and-install flags and the execution-time channel", () => | |
| 102 | Effect.gen(function* () { | |
| 103 | const cause = new Error("quit and install failed"); | |
| 104 | autoUpdaterMock.quitAndInstall.mockImplementationOnce(() => { | |
| 105 | throw cause; | |
| 106 | }); | |
| 107 | const updater = yield* ElectronUpdater.ElectronUpdater; | |
| 108 | autoUpdaterMock.channel = "alpha"; | |
| 109 | ||
| 110 | const error = yield* updater | |
| 111 | .quitAndInstall({ isSilent: true, isForceRunAfter: false }) | |
| 112 | .pipe(Effect.flip); | |
| 113 | ||
| 114 | assert.instanceOf(error, ElectronUpdater.ElectronUpdaterQuitAndInstallError); | |
| 115 | assert.isTrue(ElectronUpdater.isElectronUpdaterError(error)); | |
| 116 | assert.equal(error.channel, "alpha"); | |
| 117 | assert.equal(error.isSilent, true); | |
| 118 | assert.equal(error.isForceRunAfter, false); | |
| 119 | assert.strictEqual(error.cause, cause); | |
| 120 | assert.equal( | |
| 121 | error.message, | |
| 122 | "Electron updater failed to quit and install the update on channel alpha (silent: true, force run after: false).", | |
| 123 | ); | |
| 124 | assert.notInclude(error.message, cause.message); | |
| 125 | assert.deepEqual(autoUpdaterMock.quitAndInstall.mock.calls, [[true, false]]); | |
| 78 | 126 | }).pipe(Effect.provide(ElectronUpdater.layer)), |
| 79 | 127 | ); |
| 80 | 128 | }); |
apps/desktop/src/electron/ElectronUpdater.ts+37-21| ... | ... | @@ -10,40 +10,41 @@ type AutoUpdater = typeof autoUpdater; |
| 10 | 10 | |
| 11 | 11 | export type ElectronUpdaterFeedUrl = Parameters<AutoUpdater["setFeedURL"]>[0]; |
| 12 | 12 | |
| 13 | const electronUpdaterErrorFields = { | |
| 14 | cause: Schema.Defect(), | |
| 15 | }; | |
| 16 | ||
| 17 | 13 | export class ElectronUpdaterCheckForUpdatesError extends Schema.TaggedErrorClass<ElectronUpdaterCheckForUpdatesError>()( |
| 18 | 14 | "ElectronUpdaterCheckForUpdatesError", |
| 19 | 15 | { |
| 20 | ...electronUpdaterErrorFields, | |
| 16 | channel: Schema.NullOr(Schema.String), | |
| 17 | cause: Schema.Defect(), | |
| 21 | 18 | }, |
| 22 | 19 | ) { |
| 23 | 20 | override get message(): string { |
| 24 | return "Electron updater failed to check for updates."; | |
| 21 | return `Electron updater failed to check for updates on channel ${this.channel ?? "default"}.`; | |
| 25 | 22 | } |
| 26 | 23 | } |
| 27 | 24 | |
| 28 | 25 | export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedErrorClass<ElectronUpdaterDownloadUpdateError>()( |
| 29 | 26 | "ElectronUpdaterDownloadUpdateError", |
| 30 | 27 | { |
| 31 | ...electronUpdaterErrorFields, | |
| 28 | channel: Schema.NullOr(Schema.String), | |
| 29 | cause: Schema.Defect(), | |
| 32 | 30 | }, |
| 33 | 31 | ) { |
| 34 | 32 | override get message(): string { |
| 35 | return "Electron updater failed to download the update."; | |
| 33 | return `Electron updater failed to download the update on channel ${this.channel ?? "default"}.`; | |
| 36 | 34 | } |
| 37 | 35 | } |
| 38 | 36 | |
| 39 | 37 | export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedErrorClass<ElectronUpdaterQuitAndInstallError>()( |
| 40 | 38 | "ElectronUpdaterQuitAndInstallError", |
| 41 | 39 | { |
| 42 | ...electronUpdaterErrorFields, | |
| 40 | channel: Schema.NullOr(Schema.String), | |
| 41 | isSilent: Schema.Boolean, | |
| 42 | isForceRunAfter: Schema.Boolean, | |
| 43 | cause: Schema.Defect(), | |
| 43 | 44 | }, |
| 44 | 45 | ) { |
| 45 | 46 | override get message(): string { |
| 46 | return "Electron updater failed to quit and install the update."; | |
| 47 | return `Electron updater failed to quit and install the update on channel ${this.channel ?? "default"} (silent: ${this.isSilent}, force run after: ${this.isForceRunAfter}).`; | |
| 47 | 48 | } |
| 48 | 49 | } |
| 49 | 50 | |
| ... | ... | @@ -116,18 +117,33 @@ export const make = ElectronUpdater.of({ |
| 116 | 117 | autoUpdater.disableDifferentialDownload = value; |
| 117 | 118 | return Effect.void; |
| 118 | 119 | }), |
| 119 | checkForUpdates: Effect.tryPromise({ | |
| 120 | try: () => autoUpdater.checkForUpdates(), | |
| 121 | catch: (cause) => new ElectronUpdaterCheckForUpdatesError({ cause }), | |
| 122 | }).pipe(Effect.asVoid), | |
| 123 | downloadUpdate: Effect.tryPromise({ | |
| 124 | try: () => autoUpdater.downloadUpdate(), | |
| 125 | catch: (cause) => new ElectronUpdaterDownloadUpdateError({ cause }), | |
| 126 | }).pipe(Effect.asVoid), | |
| 120 | checkForUpdates: Effect.suspend(() => { | |
| 121 | const channel = autoUpdater.channel; | |
| 122 | return Effect.tryPromise({ | |
| 123 | try: () => autoUpdater.checkForUpdates(), | |
| 124 | catch: (cause) => new ElectronUpdaterCheckForUpdatesError({ channel, cause }), | |
| 125 | }).pipe(Effect.asVoid); | |
| 126 | }), | |
| 127 | downloadUpdate: Effect.suspend(() => { | |
| 128 | const channel = autoUpdater.channel; | |
| 129 | return Effect.tryPromise({ | |
| 130 | try: () => autoUpdater.downloadUpdate(), | |
| 131 | catch: (cause) => new ElectronUpdaterDownloadUpdateError({ channel, cause }), | |
| 132 | }).pipe(Effect.asVoid); | |
| 133 | }), | |
| 127 | 134 | quitAndInstall: ({ isSilent, isForceRunAfter }) => |
| 128 | Effect.try({ | |
| 129 | try: () => autoUpdater.quitAndInstall(isSilent, isForceRunAfter), | |
| 130 | catch: (cause) => new ElectronUpdaterQuitAndInstallError({ cause }), | |
| 135 | Effect.suspend(() => { | |
| 136 | const channel = autoUpdater.channel; | |
| 137 | return Effect.try({ | |
| 138 | try: () => autoUpdater.quitAndInstall(isSilent, isForceRunAfter), | |
| 139 | catch: (cause) => | |
| 140 | new ElectronUpdaterQuitAndInstallError({ | |
| 141 | channel, | |
| 142 | isSilent, | |
| 143 | isForceRunAfter, | |
| 144 | cause, | |
| 145 | }), | |
| 146 | }); | |
| 131 | 147 | }), |
| 132 | 148 | on: (eventName, listener) => { |
| 133 | 149 | const eventTarget = autoUpdater as unknown as { |