using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Repositories; public sealed class TaskRepositoryParentCompletionTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; public TaskRepositoryParentCompletionTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private async Task ListAsync() { var id = Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = id, Name = "L", CreatedAt = DateTime.UtcNow }); return id; } private async Task PlannedParentAsync(string listId) { var parent = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "p", Status = TaskStatus.Planned, CreatedAt = DateTime.UtcNow, CommitType = "chore", }; await _tasks.AddAsync(parent); return parent; } private async Task ChildAsync(string listId, string parentId, TaskStatus status) { var child = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "c", Status = status, CreatedAt = DateTime.UtcNow, CommitType = "chore", ParentTaskId = parentId, }; await _tasks.AddAsync(child); return child; } [Fact] public async Task TryCompleteParentAsync_AllChildrenDone_ParentBecomesDone() { var listId = await ListAsync(); var parent = await PlannedParentAsync(listId); await ChildAsync(listId, parent.Id, TaskStatus.Done); await ChildAsync(listId, parent.Id, TaskStatus.Done); await _tasks.TryCompleteParentAsync(parent.Id); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Done, loaded!.Status); Assert.NotNull(loaded.FinishedAt); } [Fact] public async Task TryCompleteParentAsync_OneFailedRestDone_ParentBecomesFailed() { var listId = await ListAsync(); var parent = await PlannedParentAsync(listId); await ChildAsync(listId, parent.Id, TaskStatus.Done); await ChildAsync(listId, parent.Id, TaskStatus.Failed); await _tasks.TryCompleteParentAsync(parent.Id); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Failed, loaded!.Status); Assert.NotNull(loaded.FinishedAt); } [Fact] public async Task TryCompleteParentAsync_OneStillRunning_ParentStaysPlanned() { var listId = await ListAsync(); var parent = await PlannedParentAsync(listId); await ChildAsync(listId, parent.Id, TaskStatus.Done); await ChildAsync(listId, parent.Id, TaskStatus.Running); await _tasks.TryCompleteParentAsync(parent.Id); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Planned, loaded!.Status); Assert.Null(loaded.FinishedAt); } [Fact] public async Task TryCompleteParentAsync_ChildStillDraft_ParentStaysPlanned() { var listId = await ListAsync(); var parent = await PlannedParentAsync(listId); await ChildAsync(listId, parent.Id, TaskStatus.Done); await ChildAsync(listId, parent.Id, TaskStatus.Draft); await _tasks.TryCompleteParentAsync(parent.Id); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Planned, loaded!.Status); } [Fact] public async Task TryCompleteParentAsync_ParentIsNotPlanned_NoChange() { var listId = await ListAsync(); var parent = await PlannedParentAsync(listId); await _ctx.Database.ExecuteSqlRawAsync("UPDATE tasks SET status = 'planning' WHERE id = {0}", parent.Id); await ChildAsync(listId, parent.Id, TaskStatus.Done); await _tasks.TryCompleteParentAsync(parent.Id); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Planning, loaded!.Status); } }