using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Planning; public sealed class PlanningChainCoordinatorTests : IDisposable { private readonly DbFixture _db = new(); private readonly TestDbContextFactory _factory; private readonly PlanningChainCoordinator _sut; private readonly string _listId; public PlanningChainCoordinatorTests() { _factory = _db.CreateFactory(); _sut = new PlanningChainCoordinator(_factory); _listId = Guid.NewGuid().ToString(); using var ctx = _factory.CreateDbContext(); ctx.Lists.Add(new ListEntity { Id = _listId, Name = "Test", CreatedAt = DateTime.UtcNow, DefaultCommitType = "chore", }); ctx.SaveChanges(); } public void Dispose() => _db.Dispose(); private async Task SeedPlanningFamilyAsync(string parentId, int childCount) { await using var ctx = _factory.CreateDbContext(); ctx.Tasks.Add(new TaskEntity { Id = parentId, ListId = _listId, Title = "Parent", CreatedAt = DateTime.UtcNow, Status = TaskStatus.Planned, }); for (int i = 0; i < childCount; i++) { ctx.Tasks.Add(new TaskEntity { Id = $"{parentId}-c{i}", ListId = _listId, Title = $"Child {i}", CreatedAt = DateTime.UtcNow, Status = TaskStatus.Manual, ParentTaskId = parentId, SortOrder = i, }); } await ctx.SaveChangesAsync(); } private async Task> GetChildrenAsync(string parentId) { await using var ctx = _factory.CreateDbContext(); return await ctx.Tasks .AsNoTracking() .Where(t => t.ParentTaskId == parentId) .OrderBy(t => t.SortOrder) .ToListAsync(); } [Fact] public async Task QueueSubtasksSequentially_SetsFirstQueued_RestWaiting() { await SeedPlanningFamilyAsync("P", 3); await _sut.QueueSubtasksSequentiallyAsync("P", default); var kids = await GetChildrenAsync("P"); Assert.Equal(TaskStatus.Queued, kids[0].Status); Assert.Equal(TaskStatus.Waiting, kids[1].Status); Assert.Equal(TaskStatus.Waiting, kids[2].Status); } [Fact] public async Task OnChildDone_FlipsNextWaitingToQueued() { await SeedPlanningFamilyAsync("P", 3); await _sut.QueueSubtasksSequentiallyAsync("P", default); // Simulate first child finishing Done. await using (var ctx = _factory.CreateDbContext()) { var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0"); first.Status = TaskStatus.Done; await ctx.SaveChangesAsync(); } var advanced = await _sut.OnChildFinishedAsync("P-c0", TaskStatus.Done, default); Assert.Equal("P-c1", advanced); var kids = await GetChildrenAsync("P"); Assert.Equal(TaskStatus.Done, kids[0].Status); Assert.Equal(TaskStatus.Queued, kids[1].Status); Assert.Equal(TaskStatus.Waiting, kids[2].Status); } [Fact] public async Task OnChildFailed_DoesNotAdvanceChain() { await SeedPlanningFamilyAsync("P", 3); await _sut.QueueSubtasksSequentiallyAsync("P", default); await using (var ctx = _factory.CreateDbContext()) { var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0"); first.Status = TaskStatus.Failed; await ctx.SaveChangesAsync(); } var advanced = await _sut.OnChildFinishedAsync("P-c0", TaskStatus.Failed, default); Assert.Null(advanced); var kids = await GetChildrenAsync("P"); Assert.Equal(TaskStatus.Failed, kids[0].Status); Assert.Equal(TaskStatus.Waiting, kids[1].Status); Assert.Equal(TaskStatus.Waiting, kids[2].Status); } [Fact] public async Task OnChildDone_LastChild_ReturnsNull() { await SeedPlanningFamilyAsync("P", 2); await _sut.QueueSubtasksSequentiallyAsync("P", default); // Mark both done, simulating chain reaching the end. await using (var ctx = _factory.CreateDbContext()) { foreach (var t in ctx.Tasks.Where(t => t.ParentTaskId == "P")) t.Status = TaskStatus.Done; await ctx.SaveChangesAsync(); } var advanced = await _sut.OnChildFinishedAsync("P-c1", TaskStatus.Done, default); Assert.Null(advanced); } [Fact] public async Task QueueSubtasksSequentially_RejectsNonManualChildren() { await SeedPlanningFamilyAsync("P", 2); // Corrupt one child to be already Queued. await using (var ctx = _factory.CreateDbContext()) { var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0"); first.Status = TaskStatus.Queued; await ctx.SaveChangesAsync(); } await Assert.ThrowsAsync( () => _sut.QueueSubtasksSequentiallyAsync("P", default)); } }