diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj index dc83e5e..c296826 100644 --- a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj @@ -2,6 +2,7 @@ + diff --git a/src/ClaudeDo.Ui/Services/UpdateCheckService.cs b/src/ClaudeDo.Ui/Services/UpdateCheckService.cs new file mode 100644 index 0000000..bd72759 --- /dev/null +++ b/src/ClaudeDo.Ui/Services/UpdateCheckService.cs @@ -0,0 +1,73 @@ +using ClaudeDo.Releases; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace ClaudeDo.Ui.Services; + +public enum UpdateCheckStatus +{ + NeverChecked, + CheckFailed, + UpToDate, + UpdateAvailable, +} + +public sealed partial class UpdateCheckService : ObservableObject +{ + private readonly IReleaseClient _releases; + + [ObservableProperty] private bool _isUpdateAvailable; + [ObservableProperty] private string? _latestVersion; + [ObservableProperty] private string _currentVersion; + [ObservableProperty] private bool _isChecking; + [ObservableProperty] private UpdateCheckStatus _lastCheckStatus = UpdateCheckStatus.NeverChecked; + + public UpdateCheckService(IReleaseClient releases, string currentVersion) + { + _releases = releases; + _currentVersion = currentVersion; + } + + public async Task CheckNowAsync(CancellationToken ct) + { + IsChecking = true; + try + { + GiteaRelease? rel; + try + { + rel = await _releases.GetLatestReleaseAsync(ct); + } + catch + { + LastCheckStatus = UpdateCheckStatus.CheckFailed; + IsUpdateAvailable = false; + return; + } + + if (rel is null) + { + LastCheckStatus = UpdateCheckStatus.CheckFailed; + IsUpdateAvailable = false; + return; + } + + var latest = (rel.TagName ?? "").TrimStart('v', 'V'); + var cmp = VersionComparer.Compare(latest, CurrentVersion); + if (cmp.IsNewer) + { + LatestVersion = latest; + IsUpdateAvailable = true; + LastCheckStatus = UpdateCheckStatus.UpdateAvailable; + } + else + { + IsUpdateAvailable = false; + LastCheckStatus = UpdateCheckStatus.UpToDate; + } + } + finally + { + IsChecking = false; + } + } +} diff --git a/tests/ClaudeDo.Ui.Tests/Services/UpdateCheckServiceTests.cs b/tests/ClaudeDo.Ui.Tests/Services/UpdateCheckServiceTests.cs new file mode 100644 index 0000000..77b5956 --- /dev/null +++ b/tests/ClaudeDo.Ui.Tests/Services/UpdateCheckServiceTests.cs @@ -0,0 +1,62 @@ +using System.Net.Http; +using ClaudeDo.Releases; +using ClaudeDo.Ui.Services; + +namespace ClaudeDo.Ui.Tests.Services; + +public class UpdateCheckServiceTests +{ + private sealed class FakeReleaseClient : IReleaseClient + { + public GiteaRelease? Release { get; set; } + public bool Throw { get; set; } + + public Task GetLatestReleaseAsync(CancellationToken ct) + { + if (Throw) throw new HttpRequestException(); + return Task.FromResult(Release); + } + + public Task DownloadAsync(string url, string destPath, IProgress progress, CancellationToken ct) + => throw new NotSupportedException(); + } + + [Fact] + public async Task Check_NewerRelease_SetsUpdateAvailable() + { + var svc = new UpdateCheckService(new FakeReleaseClient + { + Release = new GiteaRelease("v0.3.0", "r", new[] { new ReleaseAsset("ClaudeDo-0.3.0-win-x64.zip", "u", 1) }), + }, + currentVersion: "0.1.0"); + + await svc.CheckNowAsync(CancellationToken.None); + + Assert.Equal(UpdateCheckStatus.UpdateAvailable, svc.LastCheckStatus); + Assert.True(svc.IsUpdateAvailable); + Assert.Equal("0.3.0", svc.LatestVersion); + } + + [Fact] + public async Task Check_SameRelease_SetsUpToDate() + { + var svc = new UpdateCheckService(new FakeReleaseClient + { + Release = new GiteaRelease("v0.1.0", "r", new[] { new ReleaseAsset("ClaudeDo-0.1.0-win-x64.zip", "u", 1) }), + }, + currentVersion: "0.1.0"); + + await svc.CheckNowAsync(CancellationToken.None); + + Assert.Equal(UpdateCheckStatus.UpToDate, svc.LastCheckStatus); + Assert.False(svc.IsUpdateAvailable); + } + + [Fact] + public async Task Check_NetworkError_SetsCheckFailedButDoesNotThrow() + { + var svc = new UpdateCheckService(new FakeReleaseClient { Throw = true }, "0.1.0"); + await svc.CheckNowAsync(CancellationToken.None); + Assert.Equal(UpdateCheckStatus.CheckFailed, svc.LastCheckStatus); + } +}