From 6fe57f4ec87e212dcc545fd3db33eea870810377 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Mon, 30 Mar 2026 14:45:00 -0700 Subject: [PATCH] fix: checkForUpdate reports restartRequired when current patch is rolled back (#312) Previously, checkForUpdate returned upToDate after a rollback because the condition `next != null && current?.number != next.number` treated a null next patch as "up to date". After a rollback, the Rust updater correctly uninstalls the patch (next becomes null), but the app is still running the rolled-back patch (current is non-null). The simplified condition `current?.number != next?.number` correctly detects this mismatch and returns restartRequired. Fixes https://github.com/shorebirdtech/shorebird/issues/3206 --- .../lib/src/shorebird_updater_io.dart | 7 +++- .../test/src/shorebird_updater_io_test.dart | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/shorebird_code_push/lib/src/shorebird_updater_io.dart b/shorebird_code_push/lib/src/shorebird_updater_io.dart index ff8def4..8d40f40 100644 --- a/shorebird_code_push/lib/src/shorebird_updater_io.dart +++ b/shorebird_code_push/lib/src/shorebird_updater_io.dart @@ -81,7 +81,12 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater { // If no new update is available for download, see if a new patch exists // on disk that requires a restart. final (current, next) = await (readCurrentPatch(), readNextPatch()).wait; - return next != null && current?.number != next.number + // A restart is required when the current and next patches differ. This + // covers both the "new patch downloaded" case (next differs from current) + // and the "current patch was rolled back" case (current is non-null but + // next is null, meaning the app needs to restart to revert to the base + // release). + return current?.number != next?.number ? UpdateStatus.restartRequired : UpdateStatus.upToDate; } diff --git a/shorebird_code_push/test/src/shorebird_updater_io_test.dart b/shorebird_code_push/test/src/shorebird_updater_io_test.dart index e5ae595..2e71f1f 100644 --- a/shorebird_code_push/test/src/shorebird_updater_io_test.dart +++ b/shorebird_code_push/test/src/shorebird_updater_io_test.dart @@ -270,6 +270,22 @@ void main() { }); }); + group('when no patches are installed and none available', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(0); + when(updater.nextPatchNumber).thenReturn(0); + when(updater.checkForDownloadableUpdate).thenReturn(false); + shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run); + }); + + test('returns UpdateStatus.upToDate', () async { + await expectLater( + shorebirdUpdater.checkForUpdate(), + completion(equals(UpdateStatus.upToDate)), + ); + }); + }); + group('when updater installed an update and is up to date', () { setUp(() { when(updater.currentPatchNumber).thenReturn(1); @@ -286,6 +302,24 @@ void main() { }); }); + group('when current patch has been rolled back', () { + setUp(() { + // The app is currently running patch 1, but checkForDownloadableUpdate + // triggered a rollback which set next_boot_patch to None (0). + when(updater.currentPatchNumber).thenReturn(1); + when(updater.nextPatchNumber).thenReturn(0); + when(updater.checkForDownloadableUpdate).thenReturn(false); + shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run); + }); + + test('returns UpdateStatus.restartRequired', () async { + await expectLater( + shorebirdUpdater.checkForUpdate(), + completion(equals(UpdateStatus.restartRequired)), + ); + }); + }); + group('when a track is provided', () { const track = UpdateTrack.beta;