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 TasksIslandRemoveFromQueueTests : IDisposable { private readonly string _dbPath; public TasksIslandRemoveFromQueueTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_unq_{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() => new(new TestDbFactory(NewContext), worker: null); private async Task SeedParentWithChainAsync( string parentId, PlanningPhase parentPhase, params (string Id, TaskStatus Status, string? BlockedBy)[] children) { await using var db = NewContext(); db.Lists.Add(new ListEntity { Id = "list1", Name = "Default", CreatedAt = DateTime.UtcNow, }); db.Tasks.Add(new TaskEntity { Id = parentId, ListId = "list1", Title = "Parent", CreatedAt = DateTime.UtcNow, Status = TaskStatus.Idle, PlanningPhase = parentPhase, SortOrder = 0, }); for (int i = 0; i < children.Length; i++) { var c = children[i]; db.Tasks.Add(new TaskEntity { Id = c.Id, ListId = "list1", Title = $"Child {i}", CreatedAt = DateTime.UtcNow, Status = c.Status, ParentTaskId = parentId, BlockedByTaskId = c.BlockedBy, SortOrder = i + 1, }); } await db.SaveChangesAsync(); } private static async Task LoadAndWaitAsync(TasksIslandViewModel vm) { var list = new ListNavItemViewModel { Id = "user:list1", Kind = ListKind.User, Name = "Default" }; vm.LoadForList(list); var deadline = DateTime.UtcNow.AddSeconds(5); while (DateTime.UtcNow < deadline) { await Task.Delay(25); if (vm.Items.Count > 0) break; } await Task.Delay(50); } [Fact] public async Task RemoveFromQueue_PlanningPhaseNone_ParentWithQueuedChildren_CascadesUnqueue() { // Mirrors the BoxDataReader scenario: parent has PlanningPhase=None // but a queued chain of children exists under it. Click X on the parent // should clear the chain, even though planning_phase is None. await SeedParentWithChainAsync( "p1", PlanningPhase.None, ("c1", TaskStatus.Idle, null), ("c2", TaskStatus.Queued, null), ("c3", TaskStatus.Queued, "c2"), ("c4", TaskStatus.Queued, "c3")); var vm = BuildViewModel(); await LoadAndWaitAsync(vm); var parentRow = vm.Items.First(r => r.Id == "p1"); await vm.RemoveFromQueueCommand.ExecuteAsync(parentRow); await using var db = NewContext(); var kids = await db.Tasks.AsNoTracking() .Where(t => t.ParentTaskId == "p1") .OrderBy(t => t.SortOrder) .ToListAsync(); Assert.Equal(TaskStatus.Idle, kids[0].Status); Assert.All(kids.Where(k => k.Id != "c1"), k => { Assert.Equal(TaskStatus.Idle, k.Status); Assert.Null(k.BlockedByTaskId); }); } [Fact] public async Task RemoveFromQueue_QueuedTaskWithoutChildren_UnqueuesItself() { // Sanity check: existing single-task unqueue path still works. await using (var db = NewContext()) { db.Lists.Add(new ListEntity { Id = "list1", Name = "Default", CreatedAt = DateTime.UtcNow, }); db.Tasks.Add(new TaskEntity { Id = "solo", ListId = "list1", Title = "Solo", CreatedAt = DateTime.UtcNow, Status = TaskStatus.Queued, SortOrder = 0, }); await db.SaveChangesAsync(); } var vm = BuildViewModel(); await LoadAndWaitAsync(vm); var row = vm.Items.First(r => r.Id == "solo"); await vm.RemoveFromQueueCommand.ExecuteAsync(row); await using var verify = NewContext(); var t = await verify.Tasks.AsNoTracking().FirstAsync(x => x.Id == "solo"); Assert.Equal(TaskStatus.Idle, t.Status); } }