using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Data.Tests; public sealed class MoveToListTests : IDisposable { private readonly string _dbPath; private readonly ClaudeDoDbContext _ctx; public MoveToListTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_movelist_{Guid.NewGuid():N}.db"); var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={_dbPath}") .Options; _ctx = new ClaudeDoDbContext(options); _ctx.Database.EnsureCreated(); } public void Dispose() { _ctx.Dispose(); try { File.Delete(_dbPath); } catch { } try { File.Delete(_dbPath + "-wal"); } catch { } try { File.Delete(_dbPath + "-shm"); } catch { } } private async Task SeedListsAsync(params string[] ids) { foreach (var id in ids) _ctx.Lists.Add(new ListEntity { Id = id, Name = id, CreatedAt = DateTime.UtcNow }); await _ctx.SaveChangesAsync(); } [Fact] public async Task MoveToList_changes_ListId_and_appends_at_end_of_target() { await SeedListsAsync("source", "target"); _ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "source", Title = "Task", CreatedAt = DateTime.UtcNow, SortOrder = 0, }); // Existing tasks already in the target list, so we can assert the moved task lands after them. _ctx.Tasks.Add(new TaskEntity { Id = "existing1", ListId = "target", Title = "Existing 1", CreatedAt = DateTime.UtcNow, SortOrder = 0, }); _ctx.Tasks.Add(new TaskEntity { Id = "existing2", ListId = "target", Title = "Existing 2", CreatedAt = DateTime.UtcNow, SortOrder = 1, }); await _ctx.SaveChangesAsync(); _ctx.ChangeTracker.Clear(); await new TaskRepository(_ctx).MoveToListAsync("t1", "target"); var moved = await _ctx.Tasks.AsNoTracking().FirstAsync(t => t.Id == "t1"); Assert.Equal("target", moved.ListId); Assert.Equal(2, moved.SortOrder); } [Fact] public async Task MoveToList_moves_all_descendants_recursively() { await SeedListsAsync("source", "target"); _ctx.Tasks.Add(new TaskEntity { Id = "parent", ListId = "source", Title = "Parent", CreatedAt = DateTime.UtcNow, SortOrder = 0, }); _ctx.Tasks.Add(new TaskEntity { Id = "child", ListId = "source", Title = "Child", CreatedAt = DateTime.UtcNow, SortOrder = 1, ParentTaskId = "parent", }); _ctx.Tasks.Add(new TaskEntity { Id = "grandchild", ListId = "source", Title = "Grandchild", CreatedAt = DateTime.UtcNow, SortOrder = 2, ParentTaskId = "child", }); await _ctx.SaveChangesAsync(); _ctx.ChangeTracker.Clear(); await new TaskRepository(_ctx).MoveToListAsync("parent", "target"); var all = await _ctx.Tasks.AsNoTracking() .Where(t => t.Id == "parent" || t.Id == "child" || t.Id == "grandchild") .ToListAsync(); Assert.All(all, t => Assert.Equal("target", t.ListId)); } [Fact] public async Task MoveToList_first_task_in_empty_target_gets_SortOrder_zero() { await SeedListsAsync("source", "target"); _ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "source", Title = "Task", CreatedAt = DateTime.UtcNow, SortOrder = 3, }); await _ctx.SaveChangesAsync(); _ctx.ChangeTracker.Clear(); await new TaskRepository(_ctx).MoveToListAsync("t1", "target"); var moved = await _ctx.Tasks.AsNoTracking().FirstAsync(t => t.Id == "t1"); Assert.Equal("target", moved.ListId); Assert.Equal(0, moved.SortOrder); } // A corrupt parent chain must not hang the descendant walk. [Fact(Timeout = 15000)] public async Task MoveToList_survives_a_self_parenting_task() { await SeedListsAsync("source", "target"); _ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "source", Title = "Task", CreatedAt = DateTime.UtcNow, SortOrder = 0, }); await _ctx.SaveChangesAsync(); // Set the cycle after insert — the FK tolerates it, the walk must too. await _ctx.Tasks.Where(t => t.Id == "t1") .ExecuteUpdateAsync(s => s.SetProperty(t => t.ParentTaskId, "t1")); _ctx.ChangeTracker.Clear(); await new TaskRepository(_ctx).MoveToListAsync("t1", "target"); var moved = await _ctx.Tasks.AsNoTracking().FirstAsync(t => t.Id == "t1"); Assert.Equal("target", moved.ListId); } [Fact] public async Task MoveToList_throws_when_task_missing() { await SeedListsAsync("source", "target"); await Assert.ThrowsAsync(() => new TaskRepository(_ctx).MoveToListAsync("nope", "target")); } }