using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals.Settings; using Xunit; namespace ClaudeDo.Ui.Tests.ViewModels; public class SessionSkillsSettingsTabViewModelTests { private sealed class FakeWorker : StubWorkerClient { public List Installed = new(); public string? InstallUrlReceived; public string? UpdatedSourceUrl; public string? RemovedSourceUrl; public Exception? InstallError; public override Task> GetSessionSkillsAsync() => Task.FromResult(Installed); public override Task> InstallSessionSkillAsync(string url) { if (InstallError is not null) throw InstallError; InstallUrlReceived = url; Installed = Installed.Append(new SessionSkillDto("new-skill", "desc", url, "abc123", DateTimeOffset.UtcNow)).ToList(); return Task.FromResult(new List { "new-skill" }); } public override Task UpdateSessionSkillAsync(string sourceUrl) { UpdatedSourceUrl = sourceUrl; return Task.CompletedTask; } public override Task RemoveSessionSkillAsync(string sourceUrl) { RemovedSourceUrl = sourceUrl; Installed = Installed.Where(s => s.SourceUrl != sourceUrl).ToList(); return Task.CompletedTask; } } [Fact] public async Task LoadAsync_populates_skills_from_worker() { var w = new FakeWorker { Installed = new() { new SessionSkillDto("a", "desc-a", "url-a", "ref-a", DateTimeOffset.UtcNow) } }; var vm = new SessionSkillsSettingsTabViewModel(w); await vm.LoadAsync(); Assert.Single(vm.Skills); Assert.Equal("a", vm.Skills[0].Name); } [Fact] public async Task InstallAsync_installs_then_refreshes_and_sets_status() { var w = new FakeWorker(); var vm = new SessionSkillsSettingsTabViewModel(w) { InstallUrl = "https://example.com/repo.git" }; await vm.InstallCommand.ExecuteAsync(null); Assert.Equal("https://example.com/repo.git", w.InstallUrlReceived); Assert.Single(vm.Skills); Assert.Equal("new-skill", vm.Skills[0].Name); Assert.Contains("new-skill", vm.StatusMessage); Assert.Equal("", vm.InstallUrl); } [Fact] public async Task InstallAsync_surfaces_exception_message_on_failure() { // The worker actually throws a HubException on a name collision; any Exception with a // readable Message exercises the same catch-and-surface path in the view model. var w = new FakeWorker { InstallError = new InvalidOperationException("Skill 'foo' already installed from a different source.") }; var vm = new SessionSkillsSettingsTabViewModel(w) { InstallUrl = "https://example.com/dup.git" }; await vm.InstallCommand.ExecuteAsync(null); Assert.Contains("already installed", vm.StatusMessage); Assert.Empty(vm.Skills); } [Fact] public async Task InstallAsync_does_nothing_for_blank_url() { var w = new FakeWorker(); var vm = new SessionSkillsSettingsTabViewModel(w) { InstallUrl = " " }; await vm.InstallCommand.ExecuteAsync(null); Assert.Null(w.InstallUrlReceived); } [Fact] public async Task RemoveAsync_removes_then_refreshes() { var w = new FakeWorker { Installed = new() { new SessionSkillDto("a", "", "url-a", "ref-a", DateTimeOffset.UtcNow) } }; var vm = new SessionSkillsSettingsTabViewModel(w); await vm.LoadAsync(); await vm.RemoveCommand.ExecuteAsync("url-a"); Assert.Equal("url-a", w.RemovedSourceUrl); Assert.Empty(vm.Skills); } [Fact] public async Task UpdateAsync_calls_worker_then_refreshes() { var w = new FakeWorker { Installed = new() { new SessionSkillDto("a", "", "url-a", "ref-a", DateTimeOffset.UtcNow) } }; var vm = new SessionSkillsSettingsTabViewModel(w); await vm.LoadAsync(); await vm.UpdateCommand.ExecuteAsync("url-a"); Assert.Equal("url-a", w.UpdatedSourceUrl); Assert.NotEmpty(vm.StatusMessage); } }