using ClaudeDo.Data.Models; using ClaudeDo.Ui.ViewModels.Modals.Settings; namespace ClaudeDo.Ui.Tests.ViewModels; public sealed class SshSettingsTabViewModelTests { private sealed class Worker : StubWorkerClient { public List Stored = new(); public override Task> GetSshWorkspacesAsync() => Task.FromResult(Stored.ToList()); public override Task SetSshWorkspacesAsync(List workspaces) { Stored = workspaces; return Task.CompletedTask; } } [Fact] public void ParseHosts_SkipsWildcardsAndComments_SplitsMultiAlias() { var text = """ # comment Host * User root Host vps vps-alt HostName 1.2.3.4 host gitea Host *.internal Match host foo """; Assert.Equal(["vps", "vps-alt", "gitea"], SshSettingsTabViewModel.ParseHosts(text)); } [Fact] public async Task LoadAddSave_RoundTripsRows() { var worker = new Worker { Stored = [new SshWorkspace("w1", "VPS", "vps", "/srv/app")] }; var vm = new SshSettingsTabViewModel(worker, () => "Host vps"); await vm.LoadAsync(); Assert.Single(vm.Workspaces); Assert.Equal(["vps"], vm.Hosts); vm.AddCommand.Execute(null); vm.Workspaces[1].Name = "Box"; vm.Workspaces[1].Host = "box"; vm.Workspaces[1].RemotePath = "~/code"; Assert.Null(vm.Validate()); await vm.SaveAsync(); Assert.Equal(2, worker.Stored.Count); Assert.Equal("w1", worker.Stored[0].Id); // existing id preserved Assert.False(string.IsNullOrEmpty(worker.Stored[1].Id)); // new row got an id } [Fact] public async Task Validate_ReportsInvalidRow_AndRemoveDropsIt() { var vm = new SshSettingsTabViewModel(new Worker(), () => ""); await vm.LoadAsync(); vm.AddCommand.Execute(null); vm.Workspaces[0].Name = "Bad"; vm.Workspaces[0].Host = "-oProxyCommand=calc"; vm.Workspaces[0].RemotePath = "/x"; Assert.NotNull(vm.Validate()); vm.Workspaces[0].RemoveCommand.Execute(null); Assert.Empty(vm.Workspaces); Assert.Null(vm.Validate()); } [Fact] public async Task MissingSshConfig_YieldsNoHosts() { var vm = new SshSettingsTabViewModel(new Worker(), () => throw new FileNotFoundException()); await vm.LoadAsync(); Assert.Empty(vm.Hosts); } private sealed class OfflineWorker : StubWorkerClient { public bool SetCalled; public override Task> GetSshWorkspacesAsync() => throw new InvalidOperationException("offline"); public override Task SetSshWorkspacesAsync(List 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); } }