190 lines
8.0 KiB
C#
190 lines
8.0 KiB
C#
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<AgentInfo> AgentList = new();
|
|
public UpdateListConfigDto? SavedListConfig;
|
|
public UpdateTaskAgentSettingsDto? SavedTaskSettings;
|
|
|
|
public override Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult(App);
|
|
public override Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult(ListCfg);
|
|
public override Task<List<AgentInfo>> 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);
|
|
}
|
|
}
|