feat(ui): Settings-Modal auf Sidebar-Kategorien umbauen
TabControl bleibt Content-Host, TabStrip wird retempliert (nur PART_SelectedContentHost) und durch eine zweigruppige Sidebar (BASIS/ERWEITERT) ersetzt. General wird in Allgemein/Ausfuehrung/Berichte gesplittet, Session Skills (Checkbox-Liste aus General + Skills-Tab) zu einer Liste zusammengelegt, und ein Repo-Hinweisstreifen erscheint auf Worktrees/Prime Claude/Session Skills/Berichte solange keine Liste ein WorkingDir hat. Fensterbreite 580 -> 700.
This commit is contained in:
@@ -1,20 +1,56 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class SettingsModalViewModelTests
|
||||
public class SettingsModalViewModelTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath =
|
||||
Path.Combine(Path.GetTempPath(), $"claudedo_settingsmodal_test_{Guid.NewGuid():N}.db");
|
||||
|
||||
public SettingsModalViewModelTests()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_dbPath); } catch { }
|
||||
try { File.Delete(_dbPath + "-wal"); } catch { }
|
||||
try { File.Delete(_dbPath + "-shm"); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}")
|
||||
.Options;
|
||||
return new ClaudeDoDbContext(opts);
|
||||
}
|
||||
|
||||
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
private readonly Func<ClaudeDoDbContext> _create;
|
||||
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
|
||||
public ClaudeDoDbContext CreateDbContext() => _create();
|
||||
}
|
||||
|
||||
private sealed class FakeWorker : StubWorkerClient
|
||||
{
|
||||
public AppSettingsDto? AppToReturn;
|
||||
public AppSettingsDto? Saved;
|
||||
public List<SessionSkillDto> InstalledSkills = new();
|
||||
public override Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult(AppToReturn);
|
||||
public override Task UpdateAppSettingsAsync(AppSettingsDto dto) { Saved = dto; return Task.CompletedTask; }
|
||||
public override Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(InstalledSkills);
|
||||
}
|
||||
|
||||
private sealed class FakePrimeApi : IPrimeScheduleApi
|
||||
@@ -70,9 +106,9 @@ public class SettingsModalViewModelTests
|
||||
Assert.Equal(88, worker.Saved.UsageThrottleSevenDayHardPct);
|
||||
}
|
||||
|
||||
private static SettingsModalViewModel MakeVm(FakeWorker worker) =>
|
||||
private SettingsModalViewModel MakeVm(FakeWorker worker) =>
|
||||
new(worker, new PrimeClaudeTabViewModel(new FakePrimeApi()), new OnlineLoginService(),
|
||||
MakeLocalizer(), new AppSettings());
|
||||
MakeLocalizer(), new AppSettings(), new TestDbFactory(NewContext));
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_fills_usage_gate_fields_from_dto()
|
||||
@@ -100,4 +136,95 @@ public class SettingsModalViewModelTests
|
||||
Assert.Equal(42, worker.Saved!.UsageGateFiveHourPct);
|
||||
Assert.Equal(77, worker.Saved.UsageGateSevenDayPct);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasLinkedRepo_false_when_no_list_has_a_working_dir()
|
||||
{
|
||||
using (var ctx = NewContext())
|
||||
{
|
||||
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "No repo", CreatedAt = DateTime.UtcNow, WorkingDir = null });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
var vm = MakeVm(new FakeWorker());
|
||||
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.False(vm.HasLinkedRepo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasLinkedRepo_true_when_a_list_has_a_working_dir()
|
||||
{
|
||||
using (var ctx = NewContext())
|
||||
{
|
||||
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "Repo", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo" });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
var vm = MakeVm(new FakeWorker());
|
||||
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.True(vm.HasLinkedRepo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectedCategory_updates_SelectedIndex_and_ignores_null()
|
||||
{
|
||||
var vm = MakeVm(new FakeWorker());
|
||||
var worktrees = vm.Categories.Single(c => c.Key == "Worktrees");
|
||||
|
||||
vm.SelectedCategory = worktrees;
|
||||
Assert.Equal(vm.Categories.ToList().IndexOf(worktrees), vm.SelectedIndex);
|
||||
|
||||
// Simulated deselection push from the sibling sidebar ListBox — must not clobber the pick.
|
||||
vm.SelectedCategory = null;
|
||||
Assert.Equal(worktrees, vm.SelectedCategory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Categories_default_to_first_entry_and_mark_the_repo_gated_ones()
|
||||
{
|
||||
var vm = MakeVm(new FakeWorker());
|
||||
|
||||
Assert.Equal(vm.Categories[0], vm.SelectedCategory);
|
||||
Assert.True(vm.Categories.Single(c => c.Key == "Worktrees").RequiresRepo);
|
||||
Assert.True(vm.Categories.Single(c => c.Key == "Prime").RequiresRepo);
|
||||
Assert.True(vm.Categories.Single(c => c.Key == "SessionSkills").RequiresRepo);
|
||||
Assert.True(vm.Categories.Single(c => c.Key == "Reports").RequiresRepo);
|
||||
Assert.False(vm.Categories.Single(c => c.Key == "General").RequiresRepo);
|
||||
Assert.False(vm.Categories.Single(c => c.Key == "Files").RequiresRepo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SessionSkillSelection_survives_a_save_and_reload_round_trip()
|
||||
{
|
||||
// The only real regression risk of the Settings-sidebar rework: Session Skills used to be
|
||||
// two separate lists (a General-tab checkbox list + the Skills-tab management list) and is
|
||||
// now one merged list. Setting the selection, saving, and reloading into a *fresh* VM must
|
||||
// come back with the identical selection.
|
||||
var installed = new List<SessionSkillDto>
|
||||
{
|
||||
new("alpha", "", "url-alpha", "ref", DateTimeOffset.UtcNow),
|
||||
new("beta", "", "url-beta", "ref", DateTimeOffset.UtcNow),
|
||||
};
|
||||
var worker = new FakeWorker { AppToReturn = DtoWith(80, 90), InstalledSkills = installed };
|
||||
var vm = MakeVm(worker);
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.SessionSkills.Skills.Single(s => s.Name == "alpha").IsSelected = true;
|
||||
vm.SessionSkills.Skills.Single(s => s.Name == "beta").IsSelected = false;
|
||||
|
||||
await vm.SaveCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(new List<string> { "alpha" }, worker.Saved!.SessionSkills);
|
||||
|
||||
// Reload into a brand-new VM instance, as if the modal were closed and reopened.
|
||||
var worker2 = new FakeWorker { AppToReturn = worker.Saved, InstalledSkills = installed };
|
||||
var vm2 = MakeVm(worker2);
|
||||
await vm2.LoadAsync();
|
||||
|
||||
Assert.True(vm2.SessionSkills.Skills.Single(s => s.Name == "alpha").IsSelected);
|
||||
Assert.False(vm2.SessionSkills.Skills.Single(s => s.Name == "beta").IsSelected);
|
||||
Assert.Equal(new List<string> { "alpha" }, vm2.SessionSkills.SelectedSkillNames());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user