Skip to content

Commit 6fdfd16

Browse files
VortexUKclaude
andcommitted
fix: v0.1.10 wrongly showed as TooOld when only 1 release behind
User screenshot: running v0.1.10, latest v0.1.11, banner says "v0.1.10 is too old (latest v0.1.11). Uploads are blocked until you update." With StaleThresholdVersions=2 they should have seen the yellow "slightly stale" banner — one release behind is well inside the tolerance. Root cause: System.Version-shape mismatch in the staleness IndexOf. * Assembly.GetName().Version returns 4-part Version(0,1,10,0) (Revision=0) * TryParseTag("v0.1.10") returns 3-part Version(0,1,10) (Revision=-1, "unset" sentinel) * List<Version>.IndexOf uses Version.Equals, which compares ALL FOUR components → 4-part current never matches 3-part released list → idxInRecent = -1 → fell through to TooOld. The Compare-against-latest branch worked fine (Version.CompareTo treats Revision=-1 as < Revision=0 but the Build difference dominated, so we correctly detected "older"). The IndexOf is the only place where component-exact equality matters, and that's where it went wrong. Fix in two places (defensive at source and consumer): * UpdateChecker.GetCurrentAssemblyVersion now normalises to 3-part via new NormalizeToThreePart helper. * UpdateChecker.Compute also normalises both currentVersion and every element of releasedVersions on entry. Either fix alone would have closed the bug; doing both means the function is correct in isolation regardless of how the caller shapes inputs. Regression tests: * Compute_FourPartCurrentVersionStillMatchesThreePartReleased pins exactly the shape that bit production (4-part current vs 3-part released). * Compute_FourPartCurrentEqualsThreePartLatest_IsCurrentNotDevBuild pins a related shape — without normalisation, Version(0,1,11,0).CompareTo(Version(0,1,11)) > 0 would have misclassified a fully up-to-date 4-part install as DevBuild. * NormalizeToThreePart_StripsRevisionAndClampsBuild for the helper itself, including the 2-part-input edge. This ships in v0.1.12 (commit goes on top of b33e4d9, both untagged). Affects every v0.1.8+ install today — the banner has been showing TooOld whenever the user is 1 or 2 releases behind. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b33e4d9 commit 6fdfd16

2 files changed

Lines changed: 92 additions & 5 deletions

File tree

src/Core/UpdateChecker.cs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,18 @@ private static (string Url, string Sha256Hex) FindDllAsset(string releaseBlock)
284284
/// </summary>
285285
public static UpdateCheckResult Compute(Version currentVersion, IList<Version> releasedVersions)
286286
{
287+
// Normalise the caller's current version to 3-part so
288+
// Version.Equals matches against the 3-part tag-parsed
289+
// released-version list. See GetCurrentAssemblyVersion
290+
// for the full reasoning — defensive double-application
291+
// here so this function is correct in isolation too.
292+
var normalizedCurrent = currentVersion == null
293+
? new Version(0, 0, 0)
294+
: NormalizeToThreePart(currentVersion);
295+
287296
var result = new UpdateCheckResult
288297
{
289-
CurrentVersion = currentVersion?.ToString(3) ?? "",
298+
CurrentVersion = normalizedCurrent.ToString(3),
290299
};
291300

292301
if (releasedVersions == null || releasedVersions.Count == 0)
@@ -297,9 +306,11 @@ public static UpdateCheckResult Compute(Version currentVersion, IList<Version> r
297306

298307
// Sort descending so [0] is the newest, regardless of what
299308
// the caller passed in. GitHub already returns newest-first
300-
// but we don't rely on that here.
309+
// but we don't rely on that here. Normalise each entry too
310+
// in case a caller mixes shapes.
301311
var sorted = releasedVersions
302312
.Where(v => v != null)
313+
.Select(NormalizeToThreePart)
303314
.Distinct()
304315
.OrderByDescending(v => v)
305316
.ToList();
@@ -321,7 +332,7 @@ public static UpdateCheckResult Compute(Version currentVersion, IList<Version> r
321332
return result;
322333
}
323334

324-
int cmp = currentVersion.CompareTo(latest);
335+
int cmp = normalizedCurrent.CompareTo(latest);
325336
if (cmp > 0)
326337
{
327338
result.Status = UpdateStatus.DevBuild;
@@ -337,8 +348,10 @@ public static UpdateCheckResult Compute(Version currentVersion, IList<Version> r
337348
// Tolerant: SlightlyStale means within the top (Threshold+1)
338349
// released versions — so threshold=2 covers latest + the two
339350
// before it, which together are "current or within 2 back".
351+
// IndexOf uses Version.Equals which compares ALL components,
352+
// hence the normalisation on both sides above.
340353
int allowedRecent = StaleThresholdVersions + 1;
341-
int idxInRecent = sorted.Take(allowedRecent).ToList().IndexOf(currentVersion);
354+
int idxInRecent = sorted.Take(allowedRecent).ToList().IndexOf(normalizedCurrent);
342355
result.Status = idxInRecent >= 0 ? UpdateStatus.SlightlyStale : UpdateStatus.TooOld;
343356
return result;
344357
}
@@ -386,11 +399,36 @@ public static async Task<UpdateCheckResult> CheckAsync(Version currentVersion, I
386399
/// Pulls the assembly's runtime version. Centralised so the
387400
/// SettingsPanel pill and the User-Agent and the UploadClient
388401
/// gate all agree on what "current" means.
402+
///
403+
/// Normalised to a 3-part Version (Major.Minor.Build) on the
404+
/// way out. The raw Assembly.GetName().Version is always
405+
/// 4-part with Revision=0, but TryParseTag emits 3-part
406+
/// (Revision=-1, the "unset" sentinel) from "v0.1.10"-style
407+
/// tags. Version.Equals compares all four components, so
408+
/// without normalisation the staleness IndexOf check inside
409+
/// Compute would never match a 4-part current version
410+
/// against a 3-part released-version list — and a user one
411+
/// release behind would see TooOld instead of SlightlyStale.
389412
/// </summary>
390413
public static Version GetCurrentAssemblyVersion()
391414
{
392415
var asm = typeof(UpdateChecker).Assembly;
393-
return asm.GetName().Version ?? new Version(0, 0, 0);
416+
var raw = asm.GetName().Version ?? new Version(0, 0, 0);
417+
return NormalizeToThreePart(raw);
418+
}
419+
420+
/// <summary>
421+
/// Strip the Revision component so a Version compares cleanly
422+
/// against tag-parsed Versions. <c>Version.Build</c> can be -1
423+
/// (unset) on a 2-part input; we clamp to 0.
424+
/// </summary>
425+
internal static Version NormalizeToThreePart(Version v)
426+
{
427+
if (v == null) return new Version(0, 0, 0);
428+
return new Version(
429+
v.Major < 0 ? 0 : v.Major,
430+
v.Minor < 0 ? 0 : v.Minor,
431+
v.Build < 0 ? 0 : v.Build);
394432
}
395433
}
396434
}

tests/EQ2Lexicon.ACTPlugin.Tests/UpdateCheckerTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,55 @@ public void Compute_Unknown_WhenFeedIsEmpty()
131131
Assert.False(result.UploadBlocked);
132132
}
133133

134+
[Fact]
135+
public void Compute_FourPartCurrentVersionStillMatchesThreePartReleased()
136+
{
137+
// Regression test for the v0.1.10-shows-as-TooOld bug.
138+
//
139+
// Real-world setup: Assembly.GetName().Version is always
140+
// 4-part with Revision=0 (e.g. Version(0,1,10,0)).
141+
// TryParseTag("v0.1.10") emits 3-part with Revision=-1.
142+
// Version.Equals compares ALL FOUR components, so the
143+
// IndexOf inside Compute would never match → fell
144+
// through to TooOld even though the user was one release
145+
// behind. Bug was real, screenshot at the commit message.
146+
var result = UpdateChecker.Compute(
147+
new Version(0, 1, 10, 0), // 4-part — exactly what Assembly.GetName().Version returns
148+
new[]
149+
{
150+
new Version(0, 1, 11), // 3-part — exactly what TryParseTag returns
151+
new Version(0, 1, 10),
152+
new Version(0, 1, 9),
153+
});
154+
Assert.Equal(UpdateStatus.SlightlyStale, result.Status);
155+
Assert.False(result.UploadBlocked);
156+
}
157+
158+
[Fact]
159+
public void Compute_FourPartCurrentEqualsThreePartLatest_IsCurrentNotDevBuild()
160+
{
161+
// Same shape bug from a different angle: when current is
162+
// 4-part Version(0,1,11,0) and latest is 3-part Version(0,1,11),
163+
// CompareTo says 0.1.11.0 > 0.1.11 (revision 0 > revision -1)
164+
// which would (without normalisation) misclassify a fully
165+
// up-to-date install as DevBuild.
166+
var result = UpdateChecker.Compute(
167+
new Version(0, 1, 11, 0),
168+
new[] { new Version(0, 1, 11), new Version(0, 1, 10) });
169+
Assert.Equal(UpdateStatus.Current, result.Status);
170+
}
171+
172+
[Fact]
173+
public void NormalizeToThreePart_StripsRevisionAndClampsBuild()
174+
{
175+
Assert.Equal(new Version(1, 2, 3), UpdateChecker.NormalizeToThreePart(new Version(1, 2, 3, 0)));
176+
Assert.Equal(new Version(1, 2, 3), UpdateChecker.NormalizeToThreePart(new Version(1, 2, 3, 999)));
177+
// 2-part input → Build is -1, must clamp to 0.
178+
Assert.Equal(new Version(1, 2, 0), UpdateChecker.NormalizeToThreePart(new Version(1, 2)));
179+
// Null defensiveness.
180+
Assert.Equal(new Version(0, 0, 0), UpdateChecker.NormalizeToThreePart(null!));
181+
}
182+
134183
[Fact]
135184
public void Compute_HandlesUnsortedInput()
136185
{

0 commit comments

Comments
 (0)