fix(ui): ssh settings tab survives an offline worker without wiping workspaces

This commit is contained in:
mika kuns
2026-10-02 14:33:57 +02:00
parent 89ed885295
commit 8c5db95c01
2 changed files with 32 additions and 3 deletions
@@ -13,6 +13,9 @@ public sealed partial class SshSettingsTabViewModel : ViewModelBase
private readonly IWorkerClient _worker; private readonly IWorkerClient _worker;
private readonly Func<string> _readSshConfig; private readonly Func<string> _readSshConfig;
/// <summary>True once the stored workspaces were read; gates <see cref="SaveAsync"/>.</summary>
private bool _loaded;
/// <summary>Configured workspaces, edited in place.</summary> /// <summary>Configured workspaces, edited in place.</summary>
public ObservableCollection<SshWorkspaceRowViewModel> Workspaces { get; } = new(); public ObservableCollection<SshWorkspaceRowViewModel> Workspaces { get; } = new();
@@ -35,8 +38,13 @@ public sealed partial class SshSettingsTabViewModel : ViewModelBase
catch (Exception) { /* no ssh config = no suggestions, hosts can still be typed */ } catch (Exception) { /* no ssh config = no suggestions, hosts can still be typed */ }
Workspaces.Clear(); Workspaces.Clear();
foreach (var w in await _worker.GetSshWorkspacesAsync()) _loaded = false;
List<SshWorkspace> stored;
try { stored = await _worker.GetSshWorkspacesAsync(); }
catch (Exception) { return; } // worker offline: the modal already reports it, and Save must not wipe what it couldn't read
foreach (var w in stored)
Workspaces.Add(new SshWorkspaceRowViewModel(w.Id, Remove) { Name = w.Name, Host = w.Host, RemotePath = w.RemotePath }); Workspaces.Add(new SshWorkspaceRowViewModel(w.Id, Remove) { Name = w.Name, Host = w.Host, RemotePath = w.RemotePath });
_loaded = true;
} }
[RelayCommand] [RelayCommand]
@@ -53,8 +61,10 @@ public sealed partial class SshSettingsTabViewModel : ViewModelBase
return null; return null;
} }
/// <summary>Persists all rows via the worker.</summary> /// <summary>Persists all rows via the worker; a no-op when the load failed.</summary>
public Task SaveAsync() => _worker.SetSshWorkspacesAsync(Workspaces.Select(r => r.ToModel()).ToList()); public Task SaveAsync() => _loaded
? _worker.SetSshWorkspacesAsync(Workspaces.Select(r => r.ToModel()).ToList())
: Task.CompletedTask;
/// <summary>Concrete <c>Host</c> aliases from ssh config text; wildcard patterns and <c>Match</c> blocks are skipped.</summary> /// <summary>Concrete <c>Host</c> aliases from ssh config text; wildcard patterns and <c>Match</c> blocks are skipped.</summary>
public static List<string> ParseHosts(string configText) public static List<string> ParseHosts(string configText)
@@ -74,4 +74,23 @@ public sealed class SshSettingsTabViewModelTests
await vm.LoadAsync(); await vm.LoadAsync();
Assert.Empty(vm.Hosts); Assert.Empty(vm.Hosts);
} }
private sealed class OfflineWorker : StubWorkerClient
{
public bool SetCalled;
public override Task<List<SshWorkspace>> GetSshWorkspacesAsync() => throw new InvalidOperationException("offline");
public override Task SetSshWorkspacesAsync(List<SshWorkspace> workspaces) { SetCalled = true; return Task.CompletedTask; }
}
[Fact]
public async Task WorkerOffline_LoadDoesNotThrow_AndSaveDoesNotWipe()
{
var worker = new OfflineWorker();
var vm = new SshSettingsTabViewModel(worker, () => "");
await vm.LoadAsync();
await vm.SaveAsync();
Assert.False(worker.SetCalled);
}
} }