From b4dc9509cb6e3b78c2614d59eb8c88fb809330d4 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 15 Apr 2026 09:26:18 +0200 Subject: [PATCH] test(installer): pin 'unparseable version = Config' behavior + document IsNewer limits --- .../Core/InstallModeDetector.cs | 7 +++++++ .../InstallModeDetectorTests.cs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/ClaudeDo.Installer/Core/InstallModeDetector.cs b/src/ClaudeDo.Installer/Core/InstallModeDetector.cs index 69616ab..50b93c6 100644 --- a/src/ClaudeDo.Installer/Core/InstallModeDetector.cs +++ b/src/ClaudeDo.Installer/Core/InstallModeDetector.cs @@ -32,6 +32,13 @@ public sealed class InstallModeDetector return new DetectedState(InstallerMode.Config, manifest, release, latestVersion); } + /// + /// Returns true only when both versions parse as System.Version (major.minor[.build[.revision]]) + /// AND latest > current. Semver pre-release tags like "0.2.0-beta" fail to parse and are + /// treated as "not newer" — the user drops into Config mode with no update offered. + /// This is deliberate: offering an update we can't compare is worse than silently skipping it. + /// If the project starts shipping pre-release tags, revisit this. + /// private static bool IsNewer(string latest, string current) { if (!Version.TryParse(latest, out var lv)) return false; diff --git a/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs b/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs index 521be81..223a0a7 100644 --- a/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs +++ b/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs @@ -102,4 +102,23 @@ public sealed class InstallModeDetectorTests : IDisposable Assert.Equal(InstallerMode.Config, state.Mode); } + + [Fact] + public async Task Detect_Config_WhenInstalledVersion_IsUnparseable() + { + // install.json has been tampered with or written by an older installer with a + // version string we can't compare. Must not crash; must land on Config (no update). + InstallManifestStore.Write(_tempDir, + new InstallManifest("garbage", _tempDir, _tempDir, DateTimeOffset.UtcNow)); + + var fake = new FakeReleaseClient + { + Release = new GiteaRelease("v0.2.0", "v0.2.0", Array.Empty()) + }; + var detector = new InstallModeDetector(fake); + + var state = await detector.DetectAsync(_tempDir, CancellationToken.None); + + Assert.Equal(InstallerMode.Config, state.Mode); + } }