OpenListSettingsAsync/CreateListAsync used to await LoadAsync (name/agent config/list_config x2/ticket settings/ticket projects, all serial) before showing the window -- an unreachable ticket system alone could add a 10s HTTP timeout to every open. LoadAsync now runs via BeginLoad (fire-and-forget, mirrors UsageMonitorModalViewModel), the modal shows immediately and fields fill in behind it. GetListConfigAsync no longer runs twice per open -- AgentConfigEditorViewModel exposes the row it already fetched (LastLoadedListConfig) instead of the caller re-fetching it. Ticket-project loading is split into its own fire-and-forget step so a slow/unreachable ticket system can't block the rest of the fields, and its tri-state Save logic now also treats "never finished loading" like "not available" -- leaving the stored TicketProjectId link untouched instead of overwriting it with 0. Save/Delete are gated (CanExecute) on the core load being done, so a save can't land before the fields it writes are populated. Not measured with OperationTiming against a live worker in this session (no running app instance available) -- verified instead via 4 new unit tests that assert GetListConfigAsync fires exactly once and that a hung/failing ticket-projects call neither blocks LoadAsync's other fields nor corrupts the saved TicketProjectId. Before/after OperationTiming numbers and a visual pass are still open follow-ups.
129 lines
5.0 KiB
C#
129 lines
5.0 KiB
C#
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<ClaudeDoDbContext> NewFactory()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_listsettings_{Guid.NewGuid():N}.db");
|
|
ClaudeDoDbContext New() => new(new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
|
.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<Task<List<TicketProjectDto>>>? TicketProjectsFactory;
|
|
public UpdateListConfigDto? SavedListConfig;
|
|
|
|
public override Task<ListConfigDto?> GetListConfigAsync(string listId)
|
|
{
|
|
GetListConfigCalls++;
|
|
return Task.FromResult(Cfg);
|
|
}
|
|
|
|
public override Task<TicketSettingsDto?> GetTicketSettingsAsync() => Task.FromResult(TicketSettings);
|
|
|
|
public override Task<List<TicketProjectDto>> GetTicketProjectsAsync() =>
|
|
TicketProjectsFactory?.Invoke() ?? Task.FromResult(new List<TicketProjectDto>());
|
|
|
|
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<List<TicketProjectDto>>();
|
|
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<TicketProjectDto>
|
|
{
|
|
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);
|
|
}
|
|
}
|