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
This commit is contained in:
Eric Seidel
2026-03-30 14:45:00 -07:00
committed by GitHub
parent ed8cea1463
commit 6fe57f4ec8
2 changed files with 40 additions and 1 deletions
@@ -81,7 +81,12 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
// If no new update is available for download, see if a new patch exists // If no new update is available for download, see if a new patch exists
// on disk that requires a restart. // on disk that requires a restart.
final (current, next) = await (readCurrentPatch(), readNextPatch()).wait; 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.restartRequired
: UpdateStatus.upToDate; : UpdateStatus.upToDate;
} }
@@ -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', () { group('when updater installed an update and is up to date', () {
setUp(() { setUp(() {
when(updater.currentPatchNumber).thenReturn(1); 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', () { group('when a track is provided', () {
const track = UpdateTrack.beta; const track = UpdateTrack.beta;