import { assert, describe, it } from "@effect/vitest";import * as NodeServices from "@effect/platform-node/NodeServices";import type { DesktopUpdateState } from "@t3tools/contracts";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 TestClock from "effect/testing/TestClock"; import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";import * as DesktopConfig from "../app/DesktopConfig.ts";import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as ElectronUpdater from "../electron/ElectronUpdater.ts";import * as ElectronWindow from "../electron/ElectronWindow.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopState from "../app/DesktopState.ts";import * as DesktopUpdates from "./DesktopUpdates.ts"; interface UpdatesHarnessOptions { readonly checkForUpdates?: Effect.Effect< void, ElectronUpdater.ElectronUpdaterCheckForUpdatesError >; readonly beforeSetUpdateChannel?: Effect.Effect<void>; readonly setUpdateChannelError?: DesktopAppSettings.DesktopSettingsWriteError; readonly setDisableDifferentialDownload?: Effect.Effect<void>; readonly stopBackend?: Effect.Effect<void>; readonly env?: Record<string, string | undefined>;} const flushCallbacks = Effect.yieldNow; function makeHarness(options: UpdatesHarnessOptions = {}) { let checkCount = 0; let allowDowngrade = false; let fullChangelog = false; const feedUrls: ElectronUpdater.ElectronUpdaterFeedUrl[] = []; const listeners = new Map<string, Set<(...args: readonly unknown[]) => void>>(); const sentStates: DesktopUpdateState[] = []; const addListener = (eventName: string, listener: (...args: readonly unknown[]) => void) => { const eventListeners = listeners.get(eventName) ?? new Set(); eventListeners.add(listener); listeners.set(eventName, eventListeners); }; const removeListener = (eventName: string, listener: (...args: readonly unknown[]) => void) => { const eventListeners = listeners.get(eventName); if (!eventListeners) { return; } eventListeners.delete(listener); if (eventListeners.size === 0) { listeners.delete(eventName); } }; const updaterLayer = Layer.succeed(ElectronUpdater.ElectronUpdater, { setFeedURL: (options) => Effect.sync(() => { feedUrls.push(options); }), setAutoDownload: () => Effect.void, setAutoInstallOnAppQuit: () => Effect.void, setChannel: () => Effect.void, setAllowPrerelease: () => Effect.void, allowDowngrade: Effect.sync(() => allowDowngrade), setAllowDowngrade: (value) => Effect.sync(() => { allowDowngrade = value; }), setFullChangelog: (value) => Effect.sync(() => { fullChangelog = value; }), setDisableDifferentialDownload: () => options.setDisableDifferentialDownload ?? Effect.void, checkForUpdates: Effect.sync(() => { checkCount += 1; }).pipe(Effect.andThen(options.checkForUpdates ?? Effect.void)), downloadUpdate: Effect.void, quitAndInstall: () => Effect.void, on: (eventName, listener) => Effect.acquireRelease( Effect.sync(() => { addListener(eventName, listener as unknown as (...args: readonly unknown[]) => void); }), () => Effect.sync(() => { removeListener(eventName, listener as unknown as (...args: readonly unknown[]) => void); }), ).pipe(Effect.asVoid), } satisfies ElectronUpdater.ElectronUpdater["Service"]); const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, { create: () => Effect.die("unexpected BrowserWindow creation"), main: Effect.succeed(Option.none()), currentMainOrFirst: Effect.succeed(Option.none()), focusedMainOrFirst: Effect.succeed(Option.none()), setMain: () => Effect.void, clearMain: () => Effect.void, reveal: () => Effect.void, sendAll: (_channel, state) => Effect.sync(() => { sentStates.push(state as DesktopUpdateState); }), destroyAll: Effect.void, syncAllAppearance: () => Effect.void, } satisfies ElectronWindow.ElectronWindow["Service"]); const stubBackendInstance: DesktopBackendPool.DesktopBackendInstance = { id: DesktopBackendPool.PRIMARY_INSTANCE_ID, label: Effect.succeed("Windows"), start: Effect.void, stop: () => options.stopBackend ?? Effect.void, currentConfig: Effect.succeed(Option.none()), snapshot: Effect.succeed({ desiredRunning: false, ready: false, activePid: Option.none(), restartAttempt: 0, restartScheduled: false, }), waitForReady: () => Effect.succeed(true), }; const backendLayer = DesktopBackendPool.layerTest([stubBackendInstance]); const environmentLayer = DesktopEnvironment.layer({ dirname: "/repo/apps/desktop/src", homeDirectory: `/tmp/t3-desktop-updates-home-${process.pid}`, platform: "darwin", processArch: "x64", appVersion: "1.2.3", appPath: "/repo", isPackaged: true, resourcesPath: "/missing/resources", runningUnderArm64Translation: false, }).pipe( Layer.provide( Layer.mergeAll( NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`, T3CODE_DESKTOP_MOCK_UPDATES: "true", T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT: "4141", ...options.env, }), ), ), ); let testSettings: DesktopAppSettings.DesktopSettings = { ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS, }; const setUpdateChannelError = options.setUpdateChannelError; const settingsLayer = setUpdateChannelError || options.beforeSetUpdateChannel ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, { get: Effect.sync(() => testSettings), load: Effect.sync(() => testSettings), setMainWindowBounds: () => Effect.die("unexpected main window bounds update"), setServerExposureMode: () => Effect.die("unexpected server exposure update"), setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"), setUpdateChannel: (channel) => setUpdateChannelError ? Effect.fail(setUpdateChannelError) : (options.beforeSetUpdateChannel ?? Effect.void).pipe( Effect.andThen( Effect.sync(() => { const changed = testSettings.updateChannel !== channel; testSettings = { ...testSettings, updateChannel: channel, updateChannelConfiguredByUser: true, }; return { settings: testSettings, changed }; }), ), ), setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"), setWslDistro: () => Effect.die("unexpected WSL distro change"), setWslOnly: () => Effect.die("unexpected WSL-only toggle"), applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"), applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"), } satisfies DesktopAppSettings.DesktopAppSettings["Service"]) : DesktopAppSettings.layer; const layer = DesktopUpdates.layer.pipe( Layer.provideMerge(updaterLayer), Layer.provideMerge(windowLayer), Layer.provideMerge(backendLayer), Layer.provideMerge(DesktopState.layer), Layer.provideMerge(settingsLayer), Layer.provideMerge( DesktopConfig.layerTest({ T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`, T3CODE_DESKTOP_MOCK_UPDATES: "true", T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT: "4141", ...options.env, }), ), Layer.provideMerge(environmentLayer), Layer.provideMerge(NodeServices.layer), ); return { layer, checkCount: () => checkCount, feedUrls: () => feedUrls, fullChangelog: () => fullChangelog, listenerCount: () => Array.from(listeners.values()).reduce( (total, eventListeners) => total + eventListeners.size, 0, ), sentStates, emit: (eventName: string, payload?: unknown) => { for (const listener of listeners.get(eventName) ?? []) { listener(payload); } }, };} 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("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", }, ], }); 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"], }, { version: "1.2.4-nightly.20260709.765", items: ["[codex] Upgrade Clerk stack by @juliusmarminge in #3821"], }, ]); assert.deepEqual(harness.sentStates.at(-1)?.releaseNotes, state.releaseNotes); }), ).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"] }, ]); 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"] }]); 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"] }]); 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", () =>Lines 1–500 of 862. Load more
import { assert, describe, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import type { DesktopUpdateState } from "@t3tools/contracts"; 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 TestClock from "effect/testing/TestClock"; import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts"; import * as DesktopConfig from "../app/DesktopConfig.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as ElectronUpdater from "../electron/ElectronUpdater.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopState from "../app/DesktopState.ts"; import * as DesktopUpdates from "./DesktopUpdates.ts"; interface UpdatesHarnessOptions { readonly checkForUpdates?: Effect.Effect< void, ElectronUpdater.ElectronUpdaterCheckForUpdatesError >; readonly beforeSetUpdateChannel?: Effect.Effect<void>; readonly setUpdateChannelError?: DesktopAppSettings.DesktopSettingsWriteError; readonly setDisableDifferentialDownload?: Effect.Effect<void>; readonly stopBackend?: Effect.Effect<void>; readonly env?: Record<string, string | undefined>; } const flushCallbacks = Effect.yieldNow; function makeHarness(options: UpdatesHarnessOptions = {}) { let checkCount = 0; let allowDowngrade = false; let fullChangelog = false; const feedUrls: ElectronUpdater.ElectronUpdaterFeedUrl[] = []; const listeners = new Map<string, Set<(...args: readonly unknown[]) => void>>(); const sentStates: DesktopUpdateState[] = []; const addListener = (eventName: string, listener: (...args: readonly unknown[]) => void) => { const eventListeners = listeners.get(eventName) ?? new Set(); eventListeners.add(listener); listeners.set(eventName, eventListeners); }; const removeListener = (eventName: string, listener: (...args: readonly unknown[]) => void) => { const eventListeners = listeners.get(eventName); if (!eventListeners) { return; } eventListeners.delete(listener); if (eventListeners.size === 0) { listeners.delete(eventName); } }; const updaterLayer = Layer.succeed(ElectronUpdater.ElectronUpdater, { setFeedURL: (options) => Effect.sync(() => { feedUrls.push(options); }), setAutoDownload: () => Effect.void, setAutoInstallOnAppQuit: () => Effect.void, setChannel: () => Effect.void, setAllowPrerelease: () => Effect.void, allowDowngrade: Effect.sync(() => allowDowngrade), setAllowDowngrade: (value) => Effect.sync(() => { allowDowngrade = value; }), setFullChangelog: (value) => Effect.sync(() => { fullChangelog = value; }), setDisableDifferentialDownload: () => options.setDisableDifferentialDownload ?? Effect.void, checkForUpdates: Effect.sync(() => { checkCount += 1; }).pipe(Effect.andThen(options.checkForUpdates ?? Effect.void)), downloadUpdate: Effect.void, quitAndInstall: () => Effect.void, on: (eventName, listener) => Effect.acquireRelease( Effect.sync(() => { addListener(eventName, listener as unknown as (...args: readonly unknown[]) => void); }), () => Effect.sync(() => { removeListener(eventName, listener as unknown as (...args: readonly unknown[]) => void); }), ).pipe(Effect.asVoid), } satisfies ElectronUpdater.ElectronUpdater["Service"]); const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, { create: () => Effect.die("unexpected BrowserWindow creation"), main: Effect.succeed(Option.none()), currentMainOrFirst: Effect.succeed(Option.none()), focusedMainOrFirst: Effect.succeed(Option.none()), setMain: () => Effect.void, clearMain: () => Effect.void, reveal: () => Effect.void, sendAll: (_channel, state) => Effect.sync(() => { sentStates.push(state as DesktopUpdateState); }), destroyAll: Effect.void, syncAllAppearance: () => Effect.void, } satisfies ElectronWindow.ElectronWindow["Service"]); const stubBackendInstance: DesktopBackendPool.DesktopBackendInstance = { id: DesktopBackendPool.PRIMARY_INSTANCE_ID, label: Effect.succeed("Windows"), start: Effect.void, stop: () => options.stopBackend ?? Effect.void, currentConfig: Effect.succeed(Option.none()), snapshot: Effect.succeed({ desiredRunning: false, ready: false, activePid: Option.none(), restartAttempt: 0, restartScheduled: false, }), waitForReady: () => Effect.succeed(true), }; const backendLayer = DesktopBackendPool.layerTest([stubBackendInstance]); const environmentLayer = DesktopEnvironment.layer({ dirname: "/repo/apps/desktop/src", homeDirectory: `/tmp/t3-desktop-updates-home-${process.pid}`, platform: "darwin", processArch: "x64", appVersion: "1.2.3", appPath: "/repo", isPackaged: true, resourcesPath: "/missing/resources", runningUnderArm64Translation: false, }).pipe( Layer.provide( Layer.mergeAll( NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`, T3CODE_DESKTOP_MOCK_UPDATES: "true", T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT: "4141", ...options.env, }), ), ), ); let testSettings: DesktopAppSettings.DesktopSettings = { ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS, }; const setUpdateChannelError = options.setUpdateChannelError; const settingsLayer = setUpdateChannelError || options.beforeSetUpdateChannel ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, { get: Effect.sync(() => testSettings), load: Effect.sync(() => testSettings), setMainWindowBounds: () => Effect.die("unexpected main window bounds update"), setServerExposureMode: () => Effect.die("unexpected server exposure update"), setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"), setUpdateChannel: (channel) => setUpdateChannelError ? Effect.fail(setUpdateChannelError) : (options.beforeSetUpdateChannel ?? Effect.void).pipe( Effect.andThen( Effect.sync(() => { const changed = testSettings.updateChannel !== channel; testSettings = { ...testSettings, updateChannel: channel, updateChannelConfiguredByUser: true, }; return { settings: testSettings, changed }; }), ), ), setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"), setWslDistro: () => Effect.die("unexpected WSL distro change"), setWslOnly: () => Effect.die("unexpected WSL-only toggle"), applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"), applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"), } satisfies DesktopAppSettings.DesktopAppSettings["Service"]) : DesktopAppSettings.layer; const layer = DesktopUpdates.layer.pipe( Layer.provideMerge(updaterLayer), Layer.provideMerge(windowLayer), Layer.provideMerge(backendLayer), Layer.provideMerge(DesktopState.layer), Layer.provideMerge(settingsLayer), Layer.provideMerge( DesktopConfig.layerTest({ T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`, T3CODE_DESKTOP_MOCK_UPDATES: "true", T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT: "4141", ...options.env, }), ), Layer.provideMerge(environmentLayer), Layer.provideMerge(NodeServices.layer), ); return { layer, checkCount: () => checkCount, feedUrls: () => feedUrls, fullChangelog: () => fullChangelog, listenerCount: () => Array.from(listeners.values()).reduce( (total, eventListeners) => total + eventListeners.size, 0, ), sentStates, emit: (eventName: string, payload?: unknown) => { for (const listener of listeners.get(eventName) ?? []) { listener(payload); } }, }; } 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("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", }, ], }); 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"], }, { version: "1.2.4-nightly.20260709.765", items: ["[codex] Upgrade Clerk stack by @juliusmarminge in #3821"], }, ]); assert.deepEqual(harness.sentStates.at(-1)?.releaseNotes, state.releaseNotes); }), ).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"] }, ]); 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"] }]); 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"] }]); 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", () =>