feat(planning): prevent orphaned subtasks via guards + startup repair
Three coordinated guards close the orphan-creation paths: - CreateChildAsync refuses when the parent is not in a planning phase. - DiscardPlanningAsync now returns a structured DiscardPlanningOutcome and refuses when children are queued or running; callers can opt into auto-dequeuing queued kids via dequeueQueuedChildren=true. Terminal children (Done/Failed/Cancelled) are promoted to top-level instead of becoming orphans when the parent's PlanningPhase is reset. - OrphanRecovery hosted service clears ParentTaskId on any rows whose parent is missing or no longer in a planning phase on worker startup, mirroring the StaleTaskRecovery pattern. UI surfaces the block reason: a confirm dialog offers to dequeue queued children and retry; a running-children block is shown as a hard error asking the user to cancel first. WorkerClient now negotiates the JsonStringEnumConverter so the DiscardPlanningResult enum round-trips correctly over SignalR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -205,9 +205,9 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
|
||||
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);
|
||||
var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: false);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(DiscardPlanningResult.Discarded, outcome.Result);
|
||||
Assert.Null(await _tasks.GetByIdAsync(c1.Id));
|
||||
Assert.Null(await _tasks.GetByIdAsync(c2.Id));
|
||||
|
||||
@@ -226,9 +226,9 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
|
||||
var task = MakeTask(listId);
|
||||
await _tasks.AddAsync(task);
|
||||
|
||||
var ok = await _tasks.DiscardPlanningAsync(task.Id);
|
||||
var outcome = await _tasks.DiscardPlanningAsync(task.Id, dequeueQueuedChildren: false);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Equal(DiscardPlanningResult.NotInPlanning, outcome.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user