import { assert, describe, it } from "@effect/vitest";import * as Cause from "effect/Cause";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Option from "effect/Option";import * as References from "effect/References";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock"; import * as ElectronUpdater from "../electron/ElectronUpdater.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopState from "../app/DesktopState.ts";import * as DesktopUpdates from "./DesktopUpdates.ts";import { flushCallbacks, makeHarness } from "./updatesTestHarness.ts"; describe("DesktopUpdates", () => { it("preserves complete causes for update poller and event failures", () => { const cause = Cause.combine( Cause.fail(new Error("updater failed")), Cause.die(new Error("updater defect")), ); const pollerError = new DesktopUpdates.DesktopUpdatePollerError({ poller: "startup", cause, }); const eventError = new DesktopUpdates.DesktopUpdateEventHandlingError({ event: "download-progress", cause, }); const reportedError = new DesktopUpdates.DesktopUpdaterReportedError({ operation: "download", cause, }); const unexpectedActionError = new DesktopUpdates.DesktopUpdateUnexpectedActionError({ action: "install", cause, }); assert.strictEqual(pollerError.cause, cause); assert.equal(pollerError.poller, "startup"); assert.equal(pollerError.message, "Desktop update startup poller failed."); assert.strictEqual(eventError.cause, cause); assert.equal(eventError.event, "download-progress"); assert.equal(eventError.message, "Failed to handle desktop update download-progress event."); assert.strictEqual(reportedError.cause, cause); assert.equal(reportedError.operation, "download"); assert.equal(reportedError.message, "Desktop updater download operation reported an error."); assert.strictEqual(unexpectedActionError.cause, cause); assert.equal(unexpectedActionError.action, "install"); assert.equal( unexpectedActionError.message, "Desktop update install action failed unexpectedly.", ); }); it.effect("configures the updater and runs startup checks on the test clock", () => { const harness = makeHarness(); return Effect.gen(function* () { yield* Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; const state = yield* updates.getState; assert.equal(state.enabled, true); assert.equal(state.status, "idle"); assert.deepEqual(harness.feedUrls(), [ { provider: "generic", url: "http://localhost:4141" }, ]); assert.equal(harness.listenerCount(), 6); assert.equal(harness.checkCount(), 0); yield* TestClock.adjust(Duration.millis(15_000)); assert.equal(harness.checkCount(), 1); }), ); assert.equal(harness.listenerCount(), 0); }).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("subscribe delivers the latest state plus subsequent changes", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; const { latest, changes } = yield* updates.subscribe; assert.equal(latest.status, "idle"); const nextState = yield* Stream.runHead(changes).pipe(Effect.forkChild); yield* flushCallbacks; harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const observed = yield* Fiber.join(nextState); assert.equal(Option.getOrThrow(observed).status, "available"); assert.equal(Option.getOrThrow(observed).availableVersion, "1.2.4"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("updates and broadcasts state from updater events", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "available"); assert.equal(state.availableVersion, "1.2.4"); assert.isNotNull(state.checkedAt); assert.equal(harness.sentStates.at(-1)?.status, "available"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("enables nightly full changelog release notes and broadcasts summaries", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; yield* updates.setChannel("nightly"); assert.equal(harness.fullChangelog(), true); harness.emit("update-available", { version: "1.2.4-nightly.20260709.766", releaseNotes: [ { version: "1.2.4-nightly.20260709.766", note: `<h2>What's Changed</h2><ul><li>feat(client): persist offline environment data by <a>@juliusmarminge</a> in <a>#3795</a></li></ul><h2>Full Changelog</h2>`, }, { version: "1.2.4-nightly.20260709.765", note: "- [codex] Upgrade Clerk stack by @juliusmarminge in #3821", }, { version: "1.2.4-nightly.20260709.764", note: "- Change 764" }, { version: "1.2.4-nightly.20260709.763", note: "- Change 763" }, { version: "1.2.4-nightly.20260709.762", note: "- Change 762" }, { version: "1.2.4-nightly.20260709.761", note: "- Change 761" }, { version: "1.2.4-nightly.20260709.760", note: "- Change 760" }, ], }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "available"); assert.deepEqual(state.releaseNotes, [ { version: "1.2.4-nightly.20260709.766", items: ["feat(client): persist offline environment data by @juliusmarminge in #3795"], totalItems: 1, }, { version: "1.2.4-nightly.20260709.765", items: ["[codex] Upgrade Clerk stack by @juliusmarminge in #3821"], totalItems: 1, }, { version: "1.2.4-nightly.20260709.764", items: ["Change 764"], totalItems: 1 }, { version: "1.2.4-nightly.20260709.763", items: ["Change 763"], totalItems: 1 }, { version: "1.2.4-nightly.20260709.762", items: ["Change 762"], totalItems: 1 }, { version: "1.2.4-nightly.20260709.761", items: ["Change 761"], totalItems: 1 }, ]); assert.equal(state.omittedReleaseCount, 1); assert.deepEqual(harness.sentStates.at(-1)?.releaseNotes, state.releaseNotes); assert.equal(harness.sentStates.at(-1)?.omittedReleaseCount, 1); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("checks for newer releases after an update has been downloaded", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4", releaseNotes: "## What's changed\n- fix: queued update", }); yield* flushCallbacks; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; const result = yield* updates.check("poll"); assert.isTrue(result.checked); harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const unchangedState = yield* updates.getState; assert.equal(unchangedState.status, "downloaded"); assert.equal(unchangedState.downloadedVersion, "1.2.4"); assert.deepEqual(unchangedState.releaseNotes, [ { version: "1.2.4", items: ["fix: queued update"], totalItems: 1 }, ]); assert.equal(unchangedState.omittedReleaseCount, 0); const nextResult = yield* updates.check("poll"); assert.isTrue(nextResult.checked); harness.emit("update-available", { version: "1.2.5" }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "available"); assert.equal(state.availableVersion, "1.2.5"); assert.isNull(state.downloadedVersion); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("preserves a queued installer when the feed has no update", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4", releaseNotes: "## What's changed\n- fix: queued update", }); yield* flushCallbacks; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; yield* updates.check("poll"); harness.emit("update-not-available"); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "downloaded"); assert.equal(state.availableVersion, "1.2.4"); assert.equal(state.downloadedVersion, "1.2.4"); assert.deepEqual(state.releaseNotes, [ { version: "1.2.4", items: ["fix: queued update"], totalItems: 1 }, ]); assert.equal(state.omittedReleaseCount, 0); assert.equal(state.downloadPercent, 100); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("preserves a queued installer when the feed offers another channel", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4", releaseNotes: "## What's changed\n- fix: queued update", }); yield* flushCallbacks; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; yield* updates.check("poll"); harness.emit("update-available", { version: "1.2.5-nightly.20260710.1" }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "downloaded"); assert.equal(state.availableVersion, "1.2.4"); assert.equal(state.downloadedVersion, "1.2.4"); assert.deepEqual(state.releaseNotes, [ { version: "1.2.4", items: ["fix: queued update"], totalItems: 1 }, ]); assert.equal(state.omittedReleaseCount, 0); assert.equal(state.downloadPercent, 100); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect( "rejects install while a refresh check is in progress and releases the reservation", () => Effect.gen(function* () { const checkStarted = yield* Deferred.make<void>(); const releaseCheck = yield* Deferred.make<void>(); const harness = makeHarness({ checkForUpdates: Deferred.succeed(checkStarted, undefined).pipe( Effect.andThen(Deferred.await(releaseCheck)), ), }); yield* Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; const checkFiber = yield* updates.check("manual").pipe(Effect.forkScoped); yield* Deferred.await(checkStarted); const installResult = yield* updates.install; assert.isFalse(installResult.accepted); yield* Deferred.succeed(releaseCheck, undefined); const checkResult = yield* Fiber.join(checkFiber); assert.isTrue(checkResult.checked); const followUpCheck = yield* updates.check("manual"); assert.isTrue(followUpCheck.checked); assert.equal(harness.checkCount(), 2); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }), ); it.effect("rejects refresh checks while install is in progress", () => Effect.gen(function* () { const installStarted = yield* Deferred.make<void>(); const releaseInstall = yield* Deferred.make<void>(); const harness = makeHarness({ stopBackend: Deferred.succeed(installStarted, undefined).pipe( Effect.andThen(Deferred.await(releaseInstall)), ), }); yield* Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; const installFiber = yield* updates.install.pipe(Effect.forkScoped); yield* Deferred.await(installStarted); const checkResult = yield* updates.check("manual"); assert.isFalse(checkResult.checked); assert.equal(harness.checkCount(), 0); yield* Deferred.succeed(releaseInstall, undefined); const installResult = yield* Fiber.join(installFiber); assert.isTrue(installResult.accepted); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }), ); it.effect("keeps raw updater event failures out of update state", () => { const harness = makeHarness(); const cause = new Error( "request failed for https://user:secret@example.com/update?token=secret", ); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("error", cause); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "error"); assert.equal(state.message, "Desktop updater background operation reported an error."); assert.notInclude(state.message ?? "", "secret"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("preserves a queued installer after a background updater error", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; harness.emit("error", new Error("background updater failure")); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "error"); assert.equal(state.downloadedVersion, "1.2.4"); assert.isNull(state.errorContext); const result = yield* updates.install; assert.isTrue(result.accepted); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("logs bounded updater failure context without exposing the cause", () => { const cause = new Error( "request failed for https://user:secret@example.com/update?token=secret", ); const updaterError = new ElectronUpdater.ElectronUpdaterCheckForUpdatesError({ channel: null, cause, }); const harness = makeHarness({ checkForUpdates: Effect.fail(updaterError) }); const loggedAnnotations: Array<Record<string, unknown>> = []; const logger = Logger.make(({ fiber }) => { const annotations = fiber.getRef(References.CurrentLogAnnotations); if (annotations.errorTag === "ElectronUpdaterCheckForUpdatesError") { loggedAnnotations.push(annotations); } }); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; yield* updates.check("manual"); const state = yield* updates.getState; const loggedAnnotation = loggedAnnotations.at(-1); assert.isDefined(loggedAnnotation); assert.equal(loggedAnnotation.errorTag, "ElectronUpdaterCheckForUpdatesError"); assert.isNull(loggedAnnotation.channel); assert.notProperty(loggedAnnotation, "error"); assert.notInclude(Object.values(loggedAnnotation).map(String).join(" "), "secret"); assert.equal( state.message, "Electron updater failed to check for updates on channel default.", ); assert.notInclude(state.message ?? "", "secret"); }), ).pipe( Effect.provide( Layer.mergeAll( TestClock.layer(), harness.layer, Logger.layer([logger], { mergeWithExisting: false }), ), ), ); }); it.effect("recovers download state after an unexpected setup failure", () => { let disableDifferentialCalls = 0; const harness = makeHarness({ setDisableDifferentialDownload: Effect.suspend(() => { disableDifferentialCalls += 1; return disableDifferentialCalls === 1 ? Effect.void : Effect.die(new Error("download setup failed")); }), }); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const result = yield* updates.download; assert.isTrue(result.accepted); assert.isFalse(result.completed); const failedState = yield* updates.getState; assert.equal(failedState.status, "available"); assert.equal(failedState.errorContext, "download"); assert.equal(failedState.message, "Desktop update download action failed unexpectedly."); const changedState = yield* updates.setChannel("nightly"); assert.equal(changedState.channel, "nightly"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("restores download state and permits retry after interruption", () => Effect.gen(function* () { const actionStarted = yield* Deferred.make<void>(); let disableDifferentialCalls = 0; const harness = makeHarness({ setDisableDifferentialDownload: Effect.suspend(() => { disableDifferentialCalls += 1; if (disableDifferentialCalls === 1) {Lines 1–500 of 809. Load more
import { assert, describe, it } from "@effect/vitest"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as References from "effect/References"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import * as ElectronUpdater from "../electron/ElectronUpdater.ts"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopState from "../app/DesktopState.ts"; import * as DesktopUpdates from "./DesktopUpdates.ts"; import { flushCallbacks, makeHarness } from "./updatesTestHarness.ts"; describe("DesktopUpdates", () => { it("preserves complete causes for update poller and event failures", () => { const cause = Cause.combine( Cause.fail(new Error("updater failed")), Cause.die(new Error("updater defect")), ); const pollerError = new DesktopUpdates.DesktopUpdatePollerError({ poller: "startup", cause, }); const eventError = new DesktopUpdates.DesktopUpdateEventHandlingError({ event: "download-progress", cause, }); const reportedError = new DesktopUpdates.DesktopUpdaterReportedError({ operation: "download", cause, }); const unexpectedActionError = new DesktopUpdates.DesktopUpdateUnexpectedActionError({ action: "install", cause, }); assert.strictEqual(pollerError.cause, cause); assert.equal(pollerError.poller, "startup"); assert.equal(pollerError.message, "Desktop update startup poller failed."); assert.strictEqual(eventError.cause, cause); assert.equal(eventError.event, "download-progress"); assert.equal(eventError.message, "Failed to handle desktop update download-progress event."); assert.strictEqual(reportedError.cause, cause); assert.equal(reportedError.operation, "download"); assert.equal(reportedError.message, "Desktop updater download operation reported an error."); assert.strictEqual(unexpectedActionError.cause, cause); assert.equal(unexpectedActionError.action, "install"); assert.equal( unexpectedActionError.message, "Desktop update install action failed unexpectedly.", ); }); it.effect("configures the updater and runs startup checks on the test clock", () => { const harness = makeHarness(); return Effect.gen(function* () { yield* Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; const state = yield* updates.getState; assert.equal(state.enabled, true); assert.equal(state.status, "idle"); assert.deepEqual(harness.feedUrls(), [ { provider: "generic", url: "http://localhost:4141" }, ]); assert.equal(harness.listenerCount(), 6); assert.equal(harness.checkCount(), 0); yield* TestClock.adjust(Duration.millis(15_000)); assert.equal(harness.checkCount(), 1); }), ); assert.equal(harness.listenerCount(), 0); }).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("subscribe delivers the latest state plus subsequent changes", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; const { latest, changes } = yield* updates.subscribe; assert.equal(latest.status, "idle"); const nextState = yield* Stream.runHead(changes).pipe(Effect.forkChild); yield* flushCallbacks; harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const observed = yield* Fiber.join(nextState); assert.equal(Option.getOrThrow(observed).status, "available"); assert.equal(Option.getOrThrow(observed).availableVersion, "1.2.4"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("updates and broadcasts state from updater events", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "available"); assert.equal(state.availableVersion, "1.2.4"); assert.isNotNull(state.checkedAt); assert.equal(harness.sentStates.at(-1)?.status, "available"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("enables nightly full changelog release notes and broadcasts summaries", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; yield* updates.setChannel("nightly"); assert.equal(harness.fullChangelog(), true); harness.emit("update-available", { version: "1.2.4-nightly.20260709.766", releaseNotes: [ { version: "1.2.4-nightly.20260709.766", note: `<h2>What's Changed</h2><ul><li>feat(client): persist offline environment data by <a>@juliusmarminge</a> in <a>#3795</a></li></ul><h2>Full Changelog</h2>`, }, { version: "1.2.4-nightly.20260709.765", note: "- [codex] Upgrade Clerk stack by @juliusmarminge in #3821", }, { version: "1.2.4-nightly.20260709.764", note: "- Change 764" }, { version: "1.2.4-nightly.20260709.763", note: "- Change 763" }, { version: "1.2.4-nightly.20260709.762", note: "- Change 762" }, { version: "1.2.4-nightly.20260709.761", note: "- Change 761" }, { version: "1.2.4-nightly.20260709.760", note: "- Change 760" }, ], }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "available"); assert.deepEqual(state.releaseNotes, [ { version: "1.2.4-nightly.20260709.766", items: ["feat(client): persist offline environment data by @juliusmarminge in #3795"], totalItems: 1, }, { version: "1.2.4-nightly.20260709.765", items: ["[codex] Upgrade Clerk stack by @juliusmarminge in #3821"], totalItems: 1, }, { version: "1.2.4-nightly.20260709.764", items: ["Change 764"], totalItems: 1 }, { version: "1.2.4-nightly.20260709.763", items: ["Change 763"], totalItems: 1 }, { version: "1.2.4-nightly.20260709.762", items: ["Change 762"], totalItems: 1 }, { version: "1.2.4-nightly.20260709.761", items: ["Change 761"], totalItems: 1 }, ]); assert.equal(state.omittedReleaseCount, 1); assert.deepEqual(harness.sentStates.at(-1)?.releaseNotes, state.releaseNotes); assert.equal(harness.sentStates.at(-1)?.omittedReleaseCount, 1); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("checks for newer releases after an update has been downloaded", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4", releaseNotes: "## What's changed\n- fix: queued update", }); yield* flushCallbacks; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; const result = yield* updates.check("poll"); assert.isTrue(result.checked); harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const unchangedState = yield* updates.getState; assert.equal(unchangedState.status, "downloaded"); assert.equal(unchangedState.downloadedVersion, "1.2.4"); assert.deepEqual(unchangedState.releaseNotes, [ { version: "1.2.4", items: ["fix: queued update"], totalItems: 1 }, ]); assert.equal(unchangedState.omittedReleaseCount, 0); const nextResult = yield* updates.check("poll"); assert.isTrue(nextResult.checked); harness.emit("update-available", { version: "1.2.5" }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "available"); assert.equal(state.availableVersion, "1.2.5"); assert.isNull(state.downloadedVersion); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("preserves a queued installer when the feed has no update", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4", releaseNotes: "## What's changed\n- fix: queued update", }); yield* flushCallbacks; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; yield* updates.check("poll"); harness.emit("update-not-available"); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "downloaded"); assert.equal(state.availableVersion, "1.2.4"); assert.equal(state.downloadedVersion, "1.2.4"); assert.deepEqual(state.releaseNotes, [ { version: "1.2.4", items: ["fix: queued update"], totalItems: 1 }, ]); assert.equal(state.omittedReleaseCount, 0); assert.equal(state.downloadPercent, 100); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("preserves a queued installer when the feed offers another channel", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4", releaseNotes: "## What's changed\n- fix: queued update", }); yield* flushCallbacks; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; yield* updates.check("poll"); harness.emit("update-available", { version: "1.2.5-nightly.20260710.1" }); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "downloaded"); assert.equal(state.availableVersion, "1.2.4"); assert.equal(state.downloadedVersion, "1.2.4"); assert.deepEqual(state.releaseNotes, [ { version: "1.2.4", items: ["fix: queued update"], totalItems: 1 }, ]); assert.equal(state.omittedReleaseCount, 0); assert.equal(state.downloadPercent, 100); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect( "rejects install while a refresh check is in progress and releases the reservation", () => Effect.gen(function* () { const checkStarted = yield* Deferred.make<void>(); const releaseCheck = yield* Deferred.make<void>(); const harness = makeHarness({ checkForUpdates: Deferred.succeed(checkStarted, undefined).pipe( Effect.andThen(Deferred.await(releaseCheck)), ), }); yield* Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; const checkFiber = yield* updates.check("manual").pipe(Effect.forkScoped); yield* Deferred.await(checkStarted); const installResult = yield* updates.install; assert.isFalse(installResult.accepted); yield* Deferred.succeed(releaseCheck, undefined); const checkResult = yield* Fiber.join(checkFiber); assert.isTrue(checkResult.checked); const followUpCheck = yield* updates.check("manual"); assert.isTrue(followUpCheck.checked); assert.equal(harness.checkCount(), 2); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }), ); it.effect("rejects refresh checks while install is in progress", () => Effect.gen(function* () { const installStarted = yield* Deferred.make<void>(); const releaseInstall = yield* Deferred.make<void>(); const harness = makeHarness({ stopBackend: Deferred.succeed(installStarted, undefined).pipe( Effect.andThen(Deferred.await(releaseInstall)), ), }); yield* Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; const installFiber = yield* updates.install.pipe(Effect.forkScoped); yield* Deferred.await(installStarted); const checkResult = yield* updates.check("manual"); assert.isFalse(checkResult.checked); assert.equal(harness.checkCount(), 0); yield* Deferred.succeed(releaseInstall, undefined); const installResult = yield* Fiber.join(installFiber); assert.isTrue(installResult.accepted); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }), ); it.effect("keeps raw updater event failures out of update state", () => { const harness = makeHarness(); const cause = new Error( "request failed for https://user:secret@example.com/update?token=secret", ); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("error", cause); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "error"); assert.equal(state.message, "Desktop updater background operation reported an error."); assert.notInclude(state.message ?? "", "secret"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("preserves a queued installer after a background updater error", () => { const harness = makeHarness(); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; harness.emit("error", new Error("background updater failure")); yield* flushCallbacks; const state = yield* updates.getState; assert.equal(state.status, "error"); assert.equal(state.downloadedVersion, "1.2.4"); assert.isNull(state.errorContext); const result = yield* updates.install; assert.isTrue(result.accepted); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("logs bounded updater failure context without exposing the cause", () => { const cause = new Error( "request failed for https://user:secret@example.com/update?token=secret", ); const updaterError = new ElectronUpdater.ElectronUpdaterCheckForUpdatesError({ channel: null, cause, }); const harness = makeHarness({ checkForUpdates: Effect.fail(updaterError) }); const loggedAnnotations: Array<Record<string, unknown>> = []; const logger = Logger.make(({ fiber }) => { const annotations = fiber.getRef(References.CurrentLogAnnotations); if (annotations.errorTag === "ElectronUpdaterCheckForUpdatesError") { loggedAnnotations.push(annotations); } }); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; yield* updates.check("manual"); const state = yield* updates.getState; const loggedAnnotation = loggedAnnotations.at(-1); assert.isDefined(loggedAnnotation); assert.equal(loggedAnnotation.errorTag, "ElectronUpdaterCheckForUpdatesError"); assert.isNull(loggedAnnotation.channel); assert.notProperty(loggedAnnotation, "error"); assert.notInclude(Object.values(loggedAnnotation).map(String).join(" "), "secret"); assert.equal( state.message, "Electron updater failed to check for updates on channel default.", ); assert.notInclude(state.message ?? "", "secret"); }), ).pipe( Effect.provide( Layer.mergeAll( TestClock.layer(), harness.layer, Logger.layer([logger], { mergeWithExisting: false }), ), ), ); }); it.effect("recovers download state after an unexpected setup failure", () => { let disableDifferentialCalls = 0; const harness = makeHarness({ setDisableDifferentialDownload: Effect.suspend(() => { disableDifferentialCalls += 1; return disableDifferentialCalls === 1 ? Effect.void : Effect.die(new Error("download setup failed")); }), }); return Effect.scoped( Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; yield* updates.configure; harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; const result = yield* updates.download; assert.isTrue(result.accepted); assert.isFalse(result.completed); const failedState = yield* updates.getState; assert.equal(failedState.status, "available"); assert.equal(failedState.errorContext, "download"); assert.equal(failedState.message, "Desktop update download action failed unexpectedly."); const changedState = yield* updates.setChannel("nightly"); assert.equal(changedState.channel, "nightly"); }), ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); it.effect("restores download state and permits retry after interruption", () => Effect.gen(function* () { const actionStarted = yield* Deferred.make<void>(); let disableDifferentialCalls = 0; const harness = makeHarness({ setDisableDifferentialDownload: Effect.suspend(() => { disableDifferentialCalls += 1; if (disableDifferentialCalls === 1) {