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; // ClassifyItems (used by Regroup) additionally mutates every row's chain-display fields via // ApplyChainGrouping. ClearCompletedAsync only needs section membership/counts, so it goes // through the pure PartitionItems instead — this must not write ShowAsChainMember/ChainStep/ // ChainAfterLabel as a side effect of counting completed rows. public class TasksIslandClassifyItemsTests : IDisposable { private readonly string _dbPath; public TasksIslandClassifyItemsTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_ui_classify_{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 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, int expectedCount = 1) { vm.LoadForList(list); var deadline = DateTime.UtcNow.AddSeconds(5); while (DateTime.UtcNow < deadline) { await Task.Delay(25); if (vm.Items.Count >= expectedCount) break; } await Task.Delay(50); } private async Task SeedAsync( params (string Id, TaskStatus Status, string? DependsOnTaskId, int SortOrder)[] tasks) { await using var db = NewContext(); db.Lists.Add(new ListEntity { Id = "list1", Name = "Default", CreatedAt = DateTime.UtcNow }); foreach (var t in tasks) { db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = t.Id, ListId = "list1", Title = t.Id, CreatedAt = DateTime.UtcNow, Status = t.Status, DependsOnTaskId = t.DependsOnTaskId, SortOrder = t.SortOrder, }); } await db.SaveChangesAsync(); } [Fact] public async Task ClearCompleted_counting_path_leaves_chain_fields_untouched() { await SeedAsync( ("head", TaskStatus.Idle, null, 0), ("dep", TaskStatus.Idle, "head", 1)); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, UserList("list1", "Default"), expectedCount: 2); var dep = vm.Items.Single(r => r.Id == "dep"); // Sanity: the initial Regroup (during load) computed the chain correctly. Assert.True(dep.ShowAsChainMember); Assert.Equal(1, dep.ChainStep); // Corrupt the row to a sentinel distinguishable from both the correct and default value. dep.ShowAsChainMember = false; dep.ChainStep = 99; dep.ChainAfterLabel = "SENTINEL"; // Nothing is completed, so ClearCompletedAsync does nothing but count via PartitionItems. await vm.ClearCompletedCommand.ExecuteAsync(null); Assert.False(dep.ShowAsChainMember); Assert.Equal(99, dep.ChainStep); Assert.Equal("SENTINEL", dep.ChainAfterLabel); } [Fact] public async Task Regroup_still_recomputes_chain_fields() { await SeedAsync( ("head", TaskStatus.Idle, null, 0), ("dep", TaskStatus.Idle, "head", 1)); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, UserList("list1", "Default"), expectedCount: 2); var dep = vm.Items.Single(r => r.Id == "dep"); dep.ShowAsChainMember = false; dep.ChainStep = 99; dep.ChainAfterLabel = "SENTINEL"; vm.Regroup(); Assert.True(dep.ShowAsChainMember); Assert.Equal(1, dep.ChainStep); Assert.Null(dep.ChainAfterLabel); } [Fact] public async Task ClearCompleted_still_deletes_completed_rows_and_reports_count() { await SeedAsync( ("open1", TaskStatus.Idle, null, 0), ("done1", TaskStatus.Done, null, 1), ("done2", TaskStatus.Done, null, 2)); var vm = BuildViewModel(); await LoadAndWaitAsync(vm, UserList("list1", "Default"), expectedCount: 3); vm.IsShowingCompleted = true; await vm.ClearCompletedCommand.ExecuteAsync(null); Assert.DoesNotContain(vm.Items, r => r.Id is "done1" or "done2"); Assert.Contains(vm.Items, r => r.Id == "open1"); } }