using ClaudeDo.Localization; using ClaudeDo.Ui; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals.Settings; using Xunit; namespace ClaudeDo.Ui.Tests.ViewModels; public class SettingsModalViewModelTests { private sealed class FakeWorker : StubWorkerClient { public AppSettingsDto? AppToReturn; public AppSettingsDto? Saved; public override Task GetAppSettingsAsync() => Task.FromResult(AppToReturn); public override Task UpdateAppSettingsAsync(AppSettingsDto dto) { Saved = dto; return Task.CompletedTask; } } private sealed class FakePrimeApi : IPrimeScheduleApi { public Task> ListAsync() => Task.FromResult(new List()); public Task UpsertAsync(PrimeScheduleDto dto) => Task.FromResult(dto); public Task DeleteAsync(Guid id) => Task.CompletedTask; } private static Localizer MakeLocalizer() { var dir = Path.Combine(Path.GetTempPath(), "loc_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); File.WriteAllText(Path.Combine(dir, "en.json"), """{ "metadata": { "code": "en", "name": "English" }, "settings": { "save": "Save" } }"""); File.WriteAllText(Path.Combine(dir, "de.json"), """{ "metadata": { "code": "de", "name": "Deutsch" }, "settings": { "save": "Speichern" } }"""); return new Localizer(LocaleStore.Load(dir), "en"); } private static AppSettingsDto DtoWith(int fiveHourPct, int sevenDayPct) => new(DefaultClaudeInstructions: "", DefaultModel: "sonnet", DefaultMaxTurns: 30, DefaultPermissionMode: "auto", MaxParallelExecutions: 1, WorktreeStrategy: "sibling", CentralWorktreeRoot: null, WorktreeAutoCleanupEnabled: false, WorktreeAutoCleanupDays: 7, ReportExcludedPaths: null, StandupWeekday: 3, DailyPrepMaxTasks: 5, SessionSkills: null, ModelPresets: null, UsageGateFiveHourPct: fiveHourPct, UsageGateSevenDayPct: sevenDayPct); [Fact] public async Task Save_carries_dragged_throttle_stages_through_untouched() { // The throttle stages are only editable by dragging the usage-monitor gauges. Saving the // Settings modal rebuilds the whole DTO, so it must not reset them to the defaults. var worker = new FakeWorker { AppToReturn = DtoWith(65, 95) with { UsageThrottleFiveHourSoftPct = 42, UsageThrottleFiveHourHardPct = 58, UsageThrottleSevenDaySoftPct = 71, UsageThrottleSevenDayHardPct = 88, }, }; var vm = MakeVm(worker); await vm.LoadAsync(); await vm.SaveCommand.ExecuteAsync(null); Assert.NotNull(worker.Saved); Assert.Equal(42, worker.Saved!.UsageThrottleFiveHourSoftPct); Assert.Equal(58, worker.Saved.UsageThrottleFiveHourHardPct); Assert.Equal(71, worker.Saved.UsageThrottleSevenDaySoftPct); Assert.Equal(88, worker.Saved.UsageThrottleSevenDayHardPct); } private static SettingsModalViewModel MakeVm(FakeWorker worker) => new(worker, new PrimeClaudeTabViewModel(new FakePrimeApi()), new OnlineLoginService(), MakeLocalizer(), new AppSettings()); [Fact] public async Task LoadAsync_fills_usage_gate_fields_from_dto() { var worker = new FakeWorker { AppToReturn = DtoWith(65, 95) }; var vm = MakeVm(worker); await vm.LoadAsync(); Assert.Equal(65, vm.General.UsageGateFiveHourPct); Assert.Equal(95, vm.General.UsageGateSevenDayPct); } [Fact] public async Task Save_sends_usage_gate_fields_in_dto() { var worker = new FakeWorker(); var vm = MakeVm(worker); vm.General.UsageGateFiveHourPct = 42; vm.General.UsageGateSevenDayPct = 77; await vm.SaveCommand.ExecuteAsync(null); Assert.NotNull(worker.Saved); Assert.Equal(42, worker.Saved!.UsageGateFiveHourPct); Assert.Equal(77, worker.Saved.UsageGateSevenDayPct); } }