using ClaudeDo.Data.Models; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Agent; using Xunit; using TaskEntity = ClaudeDo.Data.Models.TaskEntity; namespace ClaudeDo.Ui.Tests.ViewModels; public class AgentConfigEditorViewModelTests { private sealed class FakeWorker : StubWorkerClient { public AppSettingsDto? App; public ListConfigDto? ListCfg; public List AgentList = new(); public UpdateListConfigDto? SavedListConfig; public UpdateTaskAgentSettingsDto? SavedTaskSettings; public override Task GetAppSettingsAsync() => Task.FromResult(App); public override Task GetListConfigAsync(string listId) => Task.FromResult(ListCfg); public override Task> GetAgentsAsync() => Task.FromResult(AgentList); public override Task UpdateListConfigAsync(UpdateListConfigDto dto) { SavedListConfig = dto; return Task.CompletedTask; } public override Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) { SavedTaskSettings = dto; return Task.CompletedTask; } } private static AppSettingsDto AppWith(string model, int turns) => new(DefaultClaudeInstructions: "", DefaultModel: model, DefaultMaxTurns: turns, DefaultPermissionMode: "auto", MaxParallelExecutions: 1, WorktreeStrategy: "sibling", CentralWorktreeRoot: null, WorktreeAutoCleanupEnabled: false, WorktreeAutoCleanupDays: 30, ReportExcludedPaths: null, StandupWeekday: 3, DailyPrepMaxTasks: 5); private static TaskEntity TaskWith(string? model, int? turns, string? sp, string? agentPath) => new() { Id = "t1", ListId = "l1", Title = "t", CreatedAt = DateTime.UtcNow, Model = model, MaxTurns = turns, SystemPrompt = sp, AgentPath = agentPath }; private static string Override => Loc.T("settings.inherit.overrideBadge"); private static string FromList => Loc.T("settings.inherit.inheritedFromList"); private static string FromGlobal => Loc.T("settings.inherit.inheritedFromGlobal"); // ── List scope ────────────────────────────────────────────────────────── [Fact] public async Task List_loads_config_and_global_defaults() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto("opus", "sp", null, 80) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.List); await vm.LoadForListAsync("l1"); Assert.Equal("opus", vm.Model); Assert.Equal(80m, vm.MaxTurns); Assert.Equal("sp", vm.SystemPrompt); Assert.Equal(Override, vm.ModelBadge); } [Fact] public async Task List_unset_model_inherits_from_global_only() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto(null, null, null, null) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.List); await vm.LoadForListAsync("l1"); Assert.Null(vm.Model); Assert.Equal(FromGlobal, vm.ModelBadge); // never "from list" at list scope Assert.Equal("haiku", vm.ModelInheritedHint); Assert.Equal(FromGlobal, vm.TurnsBadge); Assert.Equal("50", vm.TurnsInheritedHint); Assert.Equal("", vm.EffectiveSystemPromptHint); // list scope shows no inherited prompt } [Fact] public async Task List_reset_clears_to_inherited() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto("opus", "sp", null, 80) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.List); await vm.LoadForListAsync("l1"); vm.ResetModelCommand.Execute(null); vm.ResetTurnsCommand.Execute(null); Assert.Null(vm.Model); Assert.Null(vm.MaxTurns); Assert.Equal(FromGlobal, vm.ModelBadge); } [Fact] public async Task List_save_builds_list_config_dto_and_does_not_auto_save() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto("opus", "sp", null, 80) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.List); await vm.LoadForListAsync("l1"); vm.Model = "sonnet"; Assert.Null(w.SavedListConfig); // list scope never auto-saves await vm.SaveAsync(); Assert.NotNull(w.SavedListConfig); Assert.Equal("l1", w.SavedListConfig!.ListId); Assert.Equal("sonnet", w.SavedListConfig.Model); Assert.Equal("sp", w.SavedListConfig.SystemPrompt); Assert.Null(w.SavedListConfig.AgentPath); Assert.Equal(80, w.SavedListConfig.MaxTurns); } // ── Task scope ────────────────────────────────────────────────────────── [Fact] public async Task Task_override_model_shows_override_badge() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto("opus", "list-sp", "/x/list-agent.md", 80) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.Task); await vm.LoadForTaskAsync(TaskWith("sonnet", null, "", null)); Assert.Equal("sonnet", vm.Model); Assert.Equal(Override, vm.ModelBadge); } [Fact] public async Task Task_unset_falls_through_to_list_then_global() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto("opus", "list-sp", null, 80) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.Task); await vm.LoadForTaskAsync(TaskWith(null, null, "", null)); // model + turns inherit from the LIST tier Assert.Equal(FromList, vm.ModelBadge); Assert.Equal("opus", vm.ModelInheritedHint); Assert.Equal(FromList, vm.TurnsBadge); Assert.Equal("80", vm.TurnsInheritedHint); Assert.Equal(80, vm.EffectiveMaxTurns); Assert.Equal("list-sp", vm.EffectiveSystemPromptHint); } [Fact] public async Task Task_unset_with_no_list_tier_inherits_global() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto(null, null, null, null) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.Task); await vm.LoadForTaskAsync(TaskWith(null, null, "", null)); Assert.Equal(FromGlobal, vm.ModelBadge); Assert.Equal("haiku", vm.ModelInheritedHint); Assert.Equal(50, vm.EffectiveMaxTurns); Assert.Equal("", vm.EffectiveSystemPromptHint); } [Fact] public async Task Task_save_builds_task_agent_settings_dto() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto(null, null, null, null) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.Task); await vm.LoadForTaskAsync(TaskWith("opus", 120, "task-sp", null)); await vm.SaveAsync(); Assert.NotNull(w.SavedTaskSettings); Assert.Equal("t1", w.SavedTaskSettings!.TaskId); Assert.Equal("opus", w.SavedTaskSettings.Model); Assert.Equal("task-sp", w.SavedTaskSettings.SystemPrompt); Assert.Null(w.SavedTaskSettings.AgentPath); Assert.Equal(120, w.SavedTaskSettings.MaxTurns); } [Fact] public async Task Clear_resets_state_and_target() { var w = new FakeWorker { App = AppWith("haiku", 50), ListCfg = new ListConfigDto(null, null, null, null) }; var vm = new AgentConfigEditorViewModel(w, AgentConfigScope.Task); await vm.LoadForTaskAsync(TaskWith("opus", 120, "task-sp", null)); vm.Clear(); Assert.Null(vm.Model); Assert.Null(vm.MaxTurns); Assert.Equal("", vm.SystemPrompt); Assert.Equal("", vm.EffectiveSystemPromptHint); // After Clear, SaveAsync is a no-op (no target). w.SavedTaskSettings = null; await vm.SaveAsync(); Assert.Null(w.SavedTaskSettings); } }