using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Localization; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Islands; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Ui.Tests.ViewModels; // UX-Struktur Paket C, C1: HasNoLinkedRepo drives the no-repo banner in ListsIslandView — true // whenever no User list has a WorkingDir, recomputed after list load and after list CRUD / // list-settings save (never persisted, no dismiss). public class ListsIslandHasNoLinkedRepoTests : IDisposable { private readonly string _dbPath; public ListsIslandHasNoLinkedRepoTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_lists_norepo_test_{Guid.NewGuid():N}.db"); using var ctx = NewContext(); ctx.Database.EnsureCreated(); 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"); } 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() .UseSqlite($"Data Source={_dbPath}") .Options; return new ClaudeDoDbContext(opts); } private sealed class TestDbFactory : IDbContextFactory { private readonly Func _create; public TestDbFactory(Func create) => _create = create; public ClaudeDoDbContext CreateDbContext() => _create(); } private sealed class FakeWorker : StubWorkerClient { public override bool IsConnected => true; } private async Task SeedListAsync(string id, string name, string? workingDir = null) { await using var db = NewContext(); var entity = new ListEntity { Id = id, Name = name, WorkingDir = workingDir, CreatedAt = DateTime.UtcNow }; db.Lists.Add(entity); await db.SaveChangesAsync(); return entity; } [Fact] public async Task NoLists_HasNoLinkedRepo_IsTrue() { var vm = new ListsIslandViewModel(new TestDbFactory(NewContext)); await vm.LoadAsync(); Assert.True(vm.HasNoLinkedRepo); } [Fact] public async Task OnlyNonUserSeedLists_HasNoLinkedRepo_IsTrue() { // "My Day"/"Important"/"Planned" are excluded from UserLists by name — they must not // count as a linked repo even if (hypothetically) one had a WorkingDir. await SeedListAsync("seed-my-day", "My Day", workingDir: @"C:\some\repo"); var vm = new ListsIslandViewModel(new TestDbFactory(NewContext)); await vm.LoadAsync(); Assert.Empty(vm.UserLists); Assert.True(vm.HasNoLinkedRepo); } [Fact] public async Task UserListWithoutWorkingDir_HasNoLinkedRepo_IsTrue() { await SeedListAsync("no-dir", "No Dir"); var vm = new ListsIslandViewModel(new TestDbFactory(NewContext)); await vm.LoadAsync(); Assert.True(vm.HasNoLinkedRepo); } [Fact] public async Task UserListWithWorkingDir_HasNoLinkedRepo_IsFalse() { await SeedListAsync("has-dir", "Has Dir", workingDir: @"C:\some\repo"); var vm = new ListsIslandViewModel(new TestDbFactory(NewContext)); await vm.LoadAsync(); Assert.False(vm.HasNoLinkedRepo); } [Fact] public async Task CreateListAsync_OnEmptyBoard_KeepsHasNoLinkedRepoTrue() { var vm = new ListsIslandViewModel(new TestDbFactory(NewContext)); await vm.LoadAsync(); Assert.True(vm.HasNoLinkedRepo); // No Dialogs/service provider wired — CreateListAsync must still recompute the flag from // the newly-added (working-dir-less) row, not leave it stale. await vm.CreateListCommand.ExecuteAsync(null); Assert.True(vm.HasNoLinkedRepo); } [Fact] public async Task DeletingTheOnlyLinkedRepoList_ThenReloading_RecomputesToTrue() { var entity = await SeedListAsync("has-dir", "Has Dir", workingDir: @"C:\some\repo"); var vm = new ListsIslandViewModel(new TestDbFactory(NewContext)); await vm.LoadAsync(); Assert.False(vm.HasNoLinkedRepo); // List Settings' delete path removes the DB row, then the caller reloads (see // OpenListSettingsAsync/CreateListAsync: `if (vm.Deleted) await LoadAsync();`). await using (var db = NewContext()) { db.Lists.Remove(db.Lists.Find(entity.Id)!); await db.SaveChangesAsync(); } await vm.LoadAsync(); Assert.True(vm.HasNoLinkedRepo); } [Fact] public async Task ListSettingsSave_LinkingWorkingDir_RecomputesToFalse() { var entity = await SeedListAsync("no-dir-yet", "No Dir Yet"); var worker = new FakeWorker(); var vm = new ListsIslandViewModel(new TestDbFactory(NewContext), worker: worker); await vm.LoadAsync(); Assert.True(vm.HasNoLinkedRepo); // Simulate a List Settings save linking a working dir — the worker broadcasts the same // ListUpdated event a real save triggers, driving RefreshRowAsync. await using (var db = NewContext()) { var row = await db.Lists.FindAsync(entity.Id); row!.WorkingDir = @"C:\some\repo"; await db.SaveChangesAsync(); } worker.RaiseListUpdated(entity.Id); await Task.Delay(100); Assert.False(vm.HasNoLinkedRepo); } [Fact] public async Task ListSettingsSave_RemovingLastWorkingDir_RecomputesToTrue() { var entity = await SeedListAsync("has-dir", "Has Dir", workingDir: @"C:\some\repo"); var worker = new FakeWorker(); var vm = new ListsIslandViewModel(new TestDbFactory(NewContext), worker: worker); await vm.LoadAsync(); Assert.False(vm.HasNoLinkedRepo); await using (var db = NewContext()) { var row = await db.Lists.FindAsync(entity.Id); row!.WorkingDir = null; await db.SaveChangesAsync(); } worker.RaiseListUpdated(entity.Id); await Task.Delay(100); Assert.True(vm.HasNoLinkedRepo); } }