diff --git a/src/ClaudeDo.Data/Repositories/TaskRepository.cs b/src/ClaudeDo.Data/Repositories/TaskRepository.cs index a3709a0..8796e39 100644 --- a/src/ClaudeDo.Data/Repositories/TaskRepository.cs +++ b/src/ClaudeDo.Data/Repositories/TaskRepository.cs @@ -346,6 +346,37 @@ public sealed class TaskRepository return count; } + public async Task DiscardPlanningAsync( + string parentId, + CancellationToken ct = default) + { + using var tx = await _context.Database.BeginTransactionAsync(ct); + + var parent = await _context.Tasks + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Id == parentId, ct); + if (parent is null || parent.Status != TaskStatus.Planning) + { + await tx.RollbackAsync(ct); + return false; + } + + await _context.Tasks + .Where(t => t.ParentTaskId == parentId && t.Status == TaskStatus.Draft) + .ExecuteDeleteAsync(ct); + + await _context.Tasks + .Where(t => t.Id == parentId) + .ExecuteUpdateAsync(s => s + .SetProperty(t => t.Status, TaskStatus.Manual) + .SetProperty(t => t.PlanningSessionId, (string?)null) + .SetProperty(t => t.PlanningSessionToken, (string?)null) + .SetProperty(t => t.PlanningFinalizedAt, (DateTime?)null), ct); + + await tx.CommitAsync(ct); + return true; + } + #endregion #region Queue selection diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs index 612015b..648b97a 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs @@ -246,4 +246,40 @@ public sealed class TaskRepositoryPlanningTests : IDisposable var cLoaded = await _tasks.GetByIdAsync(c.Id); Assert.Equal(TaskStatus.Queued, cLoaded!.Status); } + + [Fact] + public async Task DiscardPlanningAsync_DeletesDraftsAndResetsParent() + { + var listId = await CreateListAsync(); + var parent = MakeTask(listId, TaskStatus.Manual); + await _tasks.AddAsync(parent); + await _tasks.SetPlanningStartedAsync(parent.Id, "tok"); + await _tasks.UpdatePlanningSessionIdAsync(parent.Id, "claude-42"); + var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null); + var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null); + + var ok = await _tasks.DiscardPlanningAsync(parent.Id); + + Assert.True(ok); + Assert.Null(await _tasks.GetByIdAsync(c1.Id)); + Assert.Null(await _tasks.GetByIdAsync(c2.Id)); + + var parentLoaded = await _tasks.GetByIdAsync(parent.Id); + Assert.Equal(TaskStatus.Manual, parentLoaded!.Status); + Assert.Null(parentLoaded.PlanningSessionId); + Assert.Null(parentLoaded.PlanningSessionToken); + Assert.Null(parentLoaded.PlanningFinalizedAt); + } + + [Fact] + public async Task DiscardPlanningAsync_OnNonPlanningTask_ReturnsFalse() + { + var listId = await CreateListAsync(); + var task = MakeTask(listId, TaskStatus.Manual); + await _tasks.AddAsync(task); + + var ok = await _tasks.DiscardPlanningAsync(task.Id); + + Assert.False(ok); + } }