using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Ui.ViewModels.Islands; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.Tests.ViewModels; public class TasksIslandRegroupTests : IDisposable { private readonly string _dbPath; public TasksIslandRegroupTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_ui_test_{Guid.NewGuid():N}.db"); 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() .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 TasksIslandViewModel BuildViewModel() { var factory = new TestDbFactory(NewContext); return new TasksIslandViewModel(factory, worker: null); } private async Task SeedPlanningWithChildAsync( TaskStatus parentStatus, TaskStatus childStatus, string parentId = "p1", string childId = "c1") { await using var db = NewContext(); var list = new ListEntity { Id = "list1", Name = "Default", CreatedAt = DateTime.UtcNow, }; db.Lists.Add(list); db.Tasks.Add(new TaskEntity { Id = parentId, ListId = list.Id, Title = "Parent", CreatedAt = DateTime.UtcNow, Status = parentStatus, SortOrder = 0, }); db.Tasks.Add(new TaskEntity { Id = childId, ListId = list.Id, Title = "Child", CreatedAt = DateTime.UtcNow, Status = childStatus, ParentTaskId = parentId, SortOrder = 1, }); await db.SaveChangesAsync(); } private static ListNavItemViewModel VirtualList(string id, string name) => new() { Id = id, Kind = ListKind.Virtual, Name = name }; private static ListNavItemViewModel UserList(string listEntityId, string name) => new() { Id = $"user:{listEntityId}", Kind = ListKind.User, Name = name }; private static async Task LoadAndWaitAsync(TasksIslandViewModel vm, ListNavItemViewModel list) { vm.LoadForList(list); // LoadForList fires a background Task; wait briefly until Items are populated // or until a timeout occurs (some tests may legitimately expect 0 items, so // we just wait a short deterministic period). var deadline = DateTime.UtcNow.AddSeconds(5); while (DateTime.UtcNow < deadline) { await Task.Delay(25); // Break out as soon as any Items present, or the background task has settled. if (vm.Items.Count > 0) break; } // One more tick for Regroup after load await Task.Delay(50); } [Fact] public async Task VirtualQueued_QueuedChildOfPlanningParent_IsNotStandaloneRow() { await SeedPlanningWithChildAsync( parentStatus: TaskStatus.Planning, childStatus: TaskStatus.Queued, parentId: "p1", childId: "c1"); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, VirtualList("virtual:queued", "Queued")); Assert.DoesNotContain(vm.Items, r => r.Id == "c1" && !r.IsChild); Assert.Contains(vm.Items, r => r.Id == "p1" && !r.IsChild); } [Fact] public async Task VirtualQueued_PlannedParentWithQueuedChild_ParentIsStandaloneRow_ChildIsNot() { await SeedPlanningWithChildAsync( parentStatus: TaskStatus.Planned, childStatus: TaskStatus.Queued, parentId: "p1", childId: "c1"); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, VirtualList("virtual:queued", "Queued")); Assert.Contains(vm.Items, r => r.Id == "p1" && !r.IsChild); Assert.DoesNotContain(vm.Items, r => r.Id == "c1" && !r.IsChild); } [Fact] public async Task VirtualRunning_RunningChildOfPlanningParent_IsNotStandaloneRow() { await SeedPlanningWithChildAsync( parentStatus: TaskStatus.Planning, childStatus: TaskStatus.Running, parentId: "p1", childId: "c1"); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, VirtualList("virtual:running", "Running")); Assert.DoesNotContain(vm.Items, r => r.Id == "c1" && !r.IsChild); Assert.Contains(vm.Items, r => r.Id == "p1" && !r.IsChild); } [Fact] public async Task Done_ChildOfOpenPlanningParent_StaysNestedUnderParent() { await SeedPlanningWithChildAsync( parentStatus: TaskStatus.Planning, childStatus: TaskStatus.Done, parentId: "p1", childId: "c1"); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, UserList("list1", "Default")); // Child with Done status under an open Planning parent should NOT go to CompletedItems Assert.DoesNotContain(vm.CompletedItems, r => r.Id == "c1"); // Child should appear nested (IsChild == true) in OpenItems Assert.Contains(vm.OpenItems, r => r.Id == "c1" && r.IsChild); } [Fact] public async Task Done_ChildOfDonePlanningParent_MovesToCompleted() { await SeedPlanningWithChildAsync( parentStatus: TaskStatus.Done, childStatus: TaskStatus.Done, parentId: "p1", childId: "c1"); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, UserList("list1", "Default")); Assert.Contains(vm.CompletedItems, r => r.Id == "p1"); Assert.Contains(vm.CompletedItems, r => r.Id == "c1"); } }