Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,77 @@ describe("DesktopUpdates", () => {
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("replaces a downloaded update when a newer release supersedes it", () => {
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" });
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;
assert.equal((yield* updates.getState).status, "downloaded");

// A later poll finds a newer release while the restart is postponed;
// the ready pill must move to it instead of advertising stale bits.
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");
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("keeps the ready pill steady when the downloaded update is still latest", () => {
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" });
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;

harness.emit("update-available", { version: "1.2.4" });
yield* flushCallbacks;

const state = yield* updates.getState;
assert.equal(state.status, "downloaded");
assert.equal(state.downloadedVersion, "1.2.4");
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("clears a downloaded update the feed no longer offers", () => {
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" });
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;

// The release was pulled (a failed nightly's cleaned-up draft): the
// stuck-forever pill from that state is the bug this guards against.
harness.emit("update-not-available");
yield* flushCallbacks;

const state = yield* updates.getState;
assert.equal(state.status, "up-to-date");
assert.isNull(state.downloadedVersion);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("uses a dev-only preview update state without configuring the updater", () => {
const harness = makeHarness({
env: {
Expand Down
37 changes: 32 additions & 5 deletions apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,18 +623,27 @@ const make = Effect.gen(function* () {
if (yield* Ref.get(updateCheckInFlightRef)) return false;

const state = yield* Ref.get(updateStateRef);
if (state.status === "downloading" || state.status === "downloaded") {
yield* logUpdaterInfo("skipping update check while update is active", {
if (state.status === "downloading") {
yield* logUpdaterInfo("skipping update check while a download is running", {
reason,
status: state.status,
});
return false;
}

// A downloaded update must not block later checks: while the user
// postpones the restart, the release it came from can be superseded by a
// newer one or pulled entirely, and a pill that can never re-check gets
// stuck advertising a version that no longer matters. The check runs
// silently in that state (no transition to "checking") so the ready pill
// doesn't flicker on every poll.
const silentCheck = state.status === "downloaded";

yield* Ref.set(updateCheckInFlightRef, true);
const checkedAt = yield* currentIsoTimestamp;
yield* setState(reduceDesktopUpdateStateOnCheckStart(state, checkedAt));
yield* logUpdaterInfo("checking for updates", { reason });
if (!silentCheck) {
yield* setState(reduceDesktopUpdateStateOnCheckStart(state, checkedAt));
}
yield* logUpdaterInfo("checking for updates", { reason, silent: silentCheck });

return yield* Effect.gen(function* () {
if (!(yield* ensurePrivateGitHubUpdateFeed())) {
Expand Down Expand Up @@ -767,6 +776,16 @@ const make = Effect.gen(function* () {
return;
}

if (state.status === "downloaded" && state.downloadedVersion === info.version) {
// The downloaded update is still the latest; keep the ready pill
// exactly as it is instead of restarting the cycle for the same
// bits.
yield* logUpdaterInfo("downloaded update is still the latest", {
version: info.version,
});
return;
}

const checkedAt = yield* currentIsoTimestamp;
yield* setState(
reduceDesktopUpdateStateOnUpdateAvailable(state, info.version, checkedAt),
Expand All @@ -786,6 +805,14 @@ const make = Effect.gen(function* () {
const handleUpdateNotAvailable = Effect.gen(function* () {
const checkedAt = yield* currentIsoTimestamp;
const state = yield* Ref.get(updateStateRef);
if (state.status === "downloaded") {
// The release the downloaded update came from is no longer offered
// (pulled or replaced by the running version); clearing to up-to-date
// retires a ready pill that would otherwise advertise it forever.
yield* logUpdaterInfo("downloaded update is no longer offered; clearing it", {
version: state.downloadedVersion,
});
}
yield* setState(reduceDesktopUpdateStateOnNoUpdate(state, checkedAt));
yield* Ref.set(lastLoggedDownloadMilestoneRef, -1);
yield* logUpdaterInfo("no updates available");
Expand Down
Loading