using ClaudeDo.Data; using ClaudeDo.Localization; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.ViewModels.Modals; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Ui.Tests.ViewModels; // Perf fix: the modal used to await seven serial hub round-trips (including a 10s-timeout // ticket-system HTTP call) before showing itself. LoadAsync must fetch the list config exactly // once and must never let a slow/failing ticket-projects call block or corrupt the rest of the // load, or the save that follows it. public class ListSettingsModalViewModelTests { static ListSettingsModalViewModelTests() { var dir = AppContext.BaseDirectory; while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales"))) dir = Path.GetDirectoryName(dir); Loc.Current = new Localizer( LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en"); } private static IDbContextFactory NewFactory() { var dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_listsettings_{Guid.NewGuid():N}.db"); ClaudeDoDbContext New() => new(new DbContextOptionsBuilder() .UseSqlite($"Data Source={dbPath}").Options); using (var ctx = New()) ctx.Database.EnsureCreated(); return new TestDbFactory(New); } private sealed class CountingWorker : StubWorkerClient { public int GetListConfigCalls; public ListConfigDto? Cfg; public TicketSettingsDto? TicketSettings = new("http://ticket.example", true); public Func>>? TicketProjectsFactory; public UpdateListConfigDto? SavedListConfig; public override Task GetListConfigAsync(string listId) { GetListConfigCalls++; return Task.FromResult(Cfg); } public override Task GetTicketSettingsAsync() => Task.FromResult(TicketSettings); public override Task> GetTicketProjectsAsync() => TicketProjectsFactory?.Invoke() ?? Task.FromResult(new List()); public override Task UpdateListConfigAsync(UpdateListConfigDto dto) { SavedListConfig = dto; return Task.CompletedTask; } } [Fact] public async Task LoadAsync_CallsGetListConfig_ExactlyOnce() { var worker = new CountingWorker { Cfg = new ListConfigDto("opus", null, null) }; var vm = new ListSettingsModalViewModel(worker, NewFactory()); await vm.LoadAsync("l1", "List One", "/repo", "feat"); Assert.Equal(1, worker.GetListConfigCalls); } [Fact] public async Task LoadAsync_TicketProjectsNeverResolving_DoesNotBlockRestOfLoad() { var pending = new TaskCompletionSource>(); var worker = new CountingWorker { Cfg = new ListConfigDto("opus", null, null, 42, null, "make test", "acceptEdits", 7, true), TicketProjectsFactory = () => pending.Task, }; var vm = new ListSettingsModalViewModel(worker, NewFactory()); // LoadAsync must complete on its own even though the ticket-projects call it kicked off // never resolves -- ticket loading is fire-and-forget precisely so a slow/unreachable // ticket system can't hold up the rest of the modal. var loadTask = vm.LoadAsync("l1", "List One", "/repo", "feat"); var completed = await Task.WhenAny(loadTask, Task.Delay(TimeSpan.FromSeconds(2))); Assert.Same(loadTask, completed); Assert.Equal("make test", vm.VerifyCommand); Assert.True(vm.PowerUseAllowed); Assert.False(vm.IsLoading); } [Fact] public async Task Save_WhileTicketProjectsUnreachable_PreservesStoredTicketLink() { var worker = new CountingWorker { Cfg = new ListConfigDto("opus", null, null, null, null, null, null, 7, false), TicketProjectsFactory = () => throw new InvalidOperationException("ticket system unreachable"), }; var vm = new ListSettingsModalViewModel(worker, NewFactory()); await vm.LoadAsync("l1", "List One", "/repo", "feat"); await vm.SaveCommand.ExecuteAsync(null); Assert.NotNull(worker.SavedListConfig); Assert.Null(worker.SavedListConfig!.TicketProjectId); } [Fact] public async Task Save_AfterSuccessfulTicketLoad_WritesSelectedTicketProject() { var worker = new CountingWorker { Cfg = new ListConfigDto("opus", null, null, null, null, null, null, 7, false), TicketProjectsFactory = () => Task.FromResult(new List { new(7, "Fix the thing", "Dev"), }), }; var vm = new ListSettingsModalViewModel(worker, NewFactory()); await vm.LoadAsync("l1", "List One", "/repo", "feat"); await vm.SaveCommand.ExecuteAsync(null); Assert.Equal(7, worker.SavedListConfig!.TicketProjectId); } }