feat(installer): replace sync ModeDetector with async InstallModeDetector
Placeholder edit to App.xaml.cs to keep the project building until Task 11 wires the new async detector. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,20 +19,12 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
var mode = ModeDetector.Detect();
|
|
||||||
_services = BuildServices();
|
_services = BuildServices();
|
||||||
|
|
||||||
Window mainWindow = mode switch
|
// TODO(Task 11): replace with async InstallModeDetector
|
||||||
|
Window mainWindow = new WizardWindow
|
||||||
{
|
{
|
||||||
InstallerMode.Wizard => new WizardWindow
|
DataContext = _services.GetRequiredService<WizardViewModel>()
|
||||||
{
|
|
||||||
DataContext = _services.GetRequiredService<WizardViewModel>()
|
|
||||||
},
|
|
||||||
InstallerMode.Settings => new SettingsWindow
|
|
||||||
{
|
|
||||||
DataContext = _services.GetRequiredService<SettingsViewModel>()
|
|
||||||
},
|
|
||||||
_ => throw new InvalidOperationException($"Unknown installer mode: {mode}")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DarkTitleBar.Apply(mainWindow);
|
DarkTitleBar.Apply(mainWindow);
|
||||||
|
|||||||
41
src/ClaudeDo.Installer/Core/InstallModeDetector.cs
Normal file
41
src/ClaudeDo.Installer/Core/InstallModeDetector.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public sealed record DetectedState(
|
||||||
|
InstallerMode Mode,
|
||||||
|
InstallManifest? Existing,
|
||||||
|
GiteaRelease? LatestRelease,
|
||||||
|
string? LatestVersion);
|
||||||
|
|
||||||
|
public sealed class InstallModeDetector
|
||||||
|
{
|
||||||
|
private readonly IReleaseClient _releases;
|
||||||
|
|
||||||
|
public InstallModeDetector(IReleaseClient releases)
|
||||||
|
{
|
||||||
|
_releases = releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DetectedState> DetectAsync(string installDir, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var manifest = InstallManifestStore.TryRead(installDir);
|
||||||
|
if (manifest is null)
|
||||||
|
return new DetectedState(InstallerMode.FreshInstall, null, null, null);
|
||||||
|
|
||||||
|
var release = await _releases.GetLatestReleaseAsync(ct);
|
||||||
|
if (release is null)
|
||||||
|
return new DetectedState(InstallerMode.Config, manifest, null, null);
|
||||||
|
|
||||||
|
var latestVersion = release.TagName.TrimStart('v', 'V');
|
||||||
|
if (IsNewer(latestVersion, manifest.Version))
|
||||||
|
return new DetectedState(InstallerMode.Update, manifest, release, latestVersion);
|
||||||
|
|
||||||
|
return new DetectedState(InstallerMode.Config, manifest, release, latestVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNewer(string latest, string current)
|
||||||
|
{
|
||||||
|
if (!Version.TryParse(latest, out var lv)) return false;
|
||||||
|
if (!Version.TryParse(current, out var cv)) return false;
|
||||||
|
return lv > cv;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,8 @@
|
|||||||
using System.IO;
|
|
||||||
using ClaudeDo.Data;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Installer.Core;
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
public enum InstallerMode { Wizard, Settings }
|
public enum InstallerMode
|
||||||
|
|
||||||
public static class ModeDetector
|
|
||||||
{
|
{
|
||||||
public static InstallerMode Detect()
|
FreshInstall, // No install.json present -> run full wizard
|
||||||
{
|
Update, // install.json present, newer release available
|
||||||
var root = Paths.AppDataRoot();
|
Config, // install.json present, no update (or API unreachable)
|
||||||
var workerConfig = Path.Combine(root, "worker.config.json");
|
|
||||||
var uiConfig = Path.Combine(root, "ui.config.json");
|
|
||||||
return File.Exists(workerConfig) && File.Exists(uiConfig)
|
|
||||||
? InstallerMode.Settings
|
|
||||||
: InstallerMode.Wizard;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
105
tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs
Normal file
105
tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Tests;
|
||||||
|
|
||||||
|
public sealed class InstallModeDetectorTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _tempDir;
|
||||||
|
|
||||||
|
public InstallModeDetectorTests()
|
||||||
|
{
|
||||||
|
_tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDoDetector-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { Directory.Delete(_tempDir, recursive: true); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeReleaseClient : IReleaseClient
|
||||||
|
{
|
||||||
|
public GiteaRelease? Release { get; set; }
|
||||||
|
public Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct) => Task.FromResult(Release);
|
||||||
|
public Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Detect_FreshInstall_WhenManifestMissing()
|
||||||
|
{
|
||||||
|
var detector = new InstallModeDetector(new FakeReleaseClient());
|
||||||
|
|
||||||
|
var state = await detector.DetectAsync(_tempDir, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(InstallerMode.FreshInstall, state.Mode);
|
||||||
|
Assert.Null(state.Existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Detect_Config_WhenManifestPresent_And_Api_Unreachable()
|
||||||
|
{
|
||||||
|
InstallManifestStore.Write(_tempDir,
|
||||||
|
new InstallManifest("0.1.0", _tempDir, _tempDir, DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
var detector = new InstallModeDetector(new FakeReleaseClient { Release = null });
|
||||||
|
|
||||||
|
var state = await detector.DetectAsync(_tempDir, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(InstallerMode.Config, state.Mode);
|
||||||
|
Assert.Equal("0.1.0", state.Existing!.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Detect_Update_WhenLatest_GreaterThan_Installed()
|
||||||
|
{
|
||||||
|
InstallManifestStore.Write(_tempDir,
|
||||||
|
new InstallManifest("0.1.0", _tempDir, _tempDir, DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
var fake = new FakeReleaseClient
|
||||||
|
{
|
||||||
|
Release = new GiteaRelease("v0.2.0", "v0.2.0", Array.Empty<ReleaseAsset>())
|
||||||
|
};
|
||||||
|
var detector = new InstallModeDetector(fake);
|
||||||
|
|
||||||
|
var state = await detector.DetectAsync(_tempDir, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(InstallerMode.Update, state.Mode);
|
||||||
|
Assert.Equal("0.1.0", state.Existing!.Version);
|
||||||
|
Assert.Equal("0.2.0", state.LatestVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Detect_Config_WhenLatest_EqualsOrOlderThan_Installed()
|
||||||
|
{
|
||||||
|
InstallManifestStore.Write(_tempDir,
|
||||||
|
new InstallManifest("0.2.0", _tempDir, _tempDir, DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
var fake = new FakeReleaseClient
|
||||||
|
{
|
||||||
|
Release = new GiteaRelease("v0.2.0", "v0.2.0", Array.Empty<ReleaseAsset>())
|
||||||
|
};
|
||||||
|
var detector = new InstallModeDetector(fake);
|
||||||
|
|
||||||
|
var state = await detector.DetectAsync(_tempDir, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(InstallerMode.Config, state.Mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Detect_Config_WhenInstalledIs_Newer_Than_Latest()
|
||||||
|
{
|
||||||
|
InstallManifestStore.Write(_tempDir,
|
||||||
|
new InstallManifest("0.3.0", _tempDir, _tempDir, DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
var fake = new FakeReleaseClient
|
||||||
|
{
|
||||||
|
Release = new GiteaRelease("v0.2.0", "v0.2.0", Array.Empty<ReleaseAsset>())
|
||||||
|
};
|
||||||
|
var detector = new InstallModeDetector(fake);
|
||||||
|
|
||||||
|
var state = await detector.DetectAsync(_tempDir, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(InstallerMode.Config, state.Mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user