From 74cc76b369b650941ec6d89ec58cc880fe8f264f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 11 May 2026 10:48:06 -0400 Subject: [PATCH] fix(flutter): preserve dev-version safety net + update version tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two test failures from the comparator rewrite: 1. "different unparseable strings → newer" was useful — branch-name builds (e.g. PackageInfo reports 'main' while server reports 'dev') should still surface the banner so the operator sees that something is misaligned. Restore that fallback: if both sides parse to all-zero components (no numeric structure on either), compare as strings. 2. "honors prerelease ordering" tested pub_semver behavior we no longer use. Replaced with tests that cover what the new comparator actually does: zero-padding for length mismatch, leading-zero normalization, 4-part date+build comparisons, and month-rollover correctness without leading zeros. --- .../lib/update/client_update_provider.dart | 12 ++++++++ .../update/client_update_provider_test.dart | 30 ++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/flutter_client/lib/update/client_update_provider.dart b/flutter_client/lib/update/client_update_provider.dart index 27689ac9..514cd2d8 100644 --- a/flutter_client/lib/update/client_update_provider.dart +++ b/flutter_client/lib/update/client_update_provider.dart @@ -71,6 +71,18 @@ bool isVersionNewer(String serverVersion, String installedVersion) { final svr = parts(serverVersion); final ins = parts(installedVersion); + + // Safety net for non-numeric versions (e.g. branch-name builds like + // "main" vs "dev"): if neither side parsed any non-zero component, + // fall back to string inequality so an operator on a dev build + // still gets the banner instead of silently matching everything. + final svrAllZero = svr.every((c) => c == 0); + final insAllZero = ins.every((c) => c == 0); + if (svrAllZero && insAllZero) { + return serverVersion.replaceFirst(RegExp(r'^v'), '') != + installedVersion.replaceFirst(RegExp(r'^v'), ''); + } + final n = svr.length > ins.length ? svr.length : ins.length; while (svr.length < n) { svr.add(0); diff --git a/flutter_client/test/update/client_update_provider_test.dart b/flutter_client/test/update/client_update_provider_test.dart index 3c362944..9433c915 100644 --- a/flutter_client/test/update/client_update_provider_test.dart +++ b/flutter_client/test/update/client_update_provider_test.dart @@ -24,17 +24,19 @@ void main() { expect(isVersionNewer('v0.1.0', 'v0.1.0'), isFalse); }); - test('honors prerelease ordering', () { - // 0.1.0 > 0.1.0-rc.1 per semver - expect(isVersionNewer('0.1.0', '0.1.0-rc.1'), isTrue); - expect(isVersionNewer('0.1.0-rc.1', '0.1.0'), isFalse); + test('zero-pads shorter version when comparing', () { + // "1.2" treated as "1.2.0" — shorter side gets zero-padded so + // comparison is component-wise. + expect(isVersionNewer('1.2.1', '1.2'), isTrue); + expect(isVersionNewer('1.2', '1.2.1'), isFalse); + expect(isVersionNewer('1.2', '1.2.0'), isFalse); }); }); - group('isVersionNewer (date-style versions parse as semver)', () { - // pub_semver is permissive with leading zeros — '2026.05.10' - // parses as 2026.5.10. So date-style tags get strict ordering, - // not the unparseable-fallback path. + group('isVersionNewer (date-style versions)', () { + // Date tags are 3- or 4-part: 2026.05.10 or 2026.05.10.1. The + // numeric comparator parses each component as int (so "05" == 5) + // and zero-pads the shorter side. test('newer date → newer', () { expect(isVersionNewer('2026.05.10', '2026.05.09'), isTrue); }); @@ -44,6 +46,18 @@ void main() { test('equal date → not newer', () { expect(isVersionNewer('2026.05.10', '2026.05.10'), isFalse); }); + test('leading zeros do not affect ordering', () { + // "2026.5.11" and "2026.05.11" parse to the same components. + // 12 > 5 numerically so month rollover stays correct. + expect(isVersionNewer('2026.5.11', '2026.05.11'), isFalse); + expect(isVersionNewer('2026.12.1', '2026.5.31'), isTrue); + }); + test('4-part build suffix breaks the tie', () { + // 2026.05.10 vs 2026.05.10.1 — first three equal, server has a + // 4th component (1), client zero-pads → server is newer. + expect(isVersionNewer('2026.05.10.1', '2026.05.10'), isTrue); + expect(isVersionNewer('2026.05.10', '2026.05.10.0'), isFalse); + }); }); group('isVersionNewer (truly non-semver fallback)', () {