From 202e3e16864e49fc655a0e47f497c41fd2720958 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 07:36:28 +0000 Subject: [PATCH 1/2] test(desktop): a failed track save keeps the current track's update offers Reproduces #34876: setChannel sets the release floor before the settings write, so a rejected save leaves the requested track's floor in place and the unchanged track's newer releases are filtered out. AI-assisted; verified with the tests and tools listed above. --- .../src/updates/DesktopUpdates.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 887f754c81042..68002f826d421 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -932,6 +932,32 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); + it.effect("keeps the current track's offers when a track switch fails to save", () => { + const harness = makeHarness({ + setUpdateChannelError: new DesktopAppSettings.DesktopSettingsWriteError({ + operation: "replace-settings-file", + path: "desktop-settings.json", + cause: new Error("rename failed"), + }), + }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + const error = yield* updates + .setChannel("nightly", { minimumVersion: "1.2.5-nightly.20261028.1" }) + .pipe(Effect.flip); + assert.equal(error._tag, "DesktopUpdateChannelPersistenceError"); + assert.equal((yield* updates.getState).channel, "latest"); + + harness.emit("update-available", { version: "1.2.4" }); + yield* flushCallbacks; + assert.equal((yield* updates.getState).availableVersion, "1.2.4"); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + it.effect("keeps a saved Stable track across a restart without offering an older release", () => { // A Nightly build whose switch to Stable did not finish before the app quit. const harness = makeHarness({ appVersion: "1.2.4-nightly.20261027.1", savedChannel: "latest" }); From 19943c548cdb28bf14daec946f77594fabfb7a23 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 07:40:05 +0000 Subject: [PATCH 2/2] fix(desktop): a failed update-track save keeps offering the current track's updates Restore the previous release floor when persisting the new update channel fails, so later checks on the unchanged track are not filtered by the requested track's minimum version. Fixes pingdotgg/t3code#34876 AI-assisted; verified with the tests and tools listed above. --- apps/desktop/src/updates/DesktopUpdates.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index 14860c238ab14..0f52ec249c2c4 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -1229,7 +1229,10 @@ export const make = Effect.gen(function* () { } return yield* Effect.gen(function* () { - yield* Ref.set(releaseFloorRef, Option.fromNullishOr(options?.minimumVersion)); + const previousFloor = yield* Ref.getAndSet( + releaseFloorRef, + Option.fromNullishOr(options?.minimumVersion), + ); const state = yield* Ref.get(updateStateRef); const retained = state.downloadedVersion ?? state.availableVersion; // A build from another track re-checks, so retrying a track switch finds an older release. @@ -1242,13 +1245,12 @@ export const make = Effect.gen(function* () { return state; } - yield* desktopSettings - .setUpdateChannel(nextChannel) - .pipe( - Effect.mapError( - (cause) => new DesktopUpdateChannelPersistenceError({ channel: nextChannel, cause }), - ), - ); + yield* desktopSettings.setUpdateChannel(nextChannel).pipe( + Effect.onError(() => Ref.set(releaseFloorRef, previousFloor)), + Effect.mapError( + (cause) => new DesktopUpdateChannelPersistenceError({ channel: nextChannel, cause }), + ), + ); const disabledReason = yield* resolveDisabledReason; yield* setState(yield* baseUpdateState(nextChannel, disabledReason));