fix(children): exempt improvement children from orphan-dequeue sweep

This commit is contained in:
mika kuns
2026-06-04 15:35:06 +02:00
parent 6fdf04d6a0
commit f25c7599bd
2 changed files with 29 additions and 4 deletions

View File

@@ -393,6 +393,9 @@ public sealed class TaskRepository
{
var orphanIds = await _context.Tasks
.Where(t => t.ParentTaskId != null && t.Status == TaskStatus.Queued)
// Agent-suggested improvement children (CreatedBy == ParentTaskId) legitimately
// queue under a non-planning parent — they are not orphaned planning-chain members.
.Where(t => t.CreatedBy == null || t.CreatedBy != t.ParentTaskId)
.Where(t => !_context.Tasks.Any(p =>
p.Id == t.ParentTaskId && p.PlanningPhase != PlanningPhase.None))
.Select(t => t.Id)

View File

@@ -61,15 +61,18 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
// --- CreateChildAsync validation ---
[Fact]
public async Task CreateChildAsync_Throws_When_Parent_Has_No_Planning_Phase()
public async Task CreateChildAsync_Succeeds_On_NonPlanning_Parent_For_Improvement_Children()
{
// The planning-phase guard was removed: improvement children attach to a
// non-planning parent, with CreatedBy stamped to the parent id by the caller.
var listId = await CreateListAsync();
var parent = MakeTask(listId, phase: PlanningPhase.None);
await _tasks.AddAsync(parent);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => _tasks.CreateChildAsync(parent.Id, "child", null, null));
Assert.Contains("not in a planning phase", ex.Message);
var child = await _tasks.CreateChildAsync(parent.Id, "child", null, null, createdBy: parent.Id);
Assert.Equal(parent.Id, child.ParentTaskId);
Assert.Equal(parent.Id, child.CreatedBy);
Assert.Equal(TaskStatus.Idle, child.Status);
}
[Fact]
@@ -201,6 +204,25 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
Assert.Equal(parent.Id, reloaded.ParentTaskId); // lineage stays
}
[Fact]
public async Task Dequeue_Leaves_Queued_Improvement_Children_Alone()
{
// Improvement children (CreatedBy == ParentTaskId) queue under a non-planning
// parent on purpose — the orphan sweep must NOT reset them.
var listId = await CreateListAsync();
var parent = MakeTask(listId, phase: PlanningPhase.None);
await _tasks.AddAsync(parent);
var child = MakeTask(listId, status: TaskStatus.Queued);
child.ParentTaskId = parent.Id;
child.CreatedBy = parent.Id;
await _tasks.AddAsync(child);
var dequeued = await _tasks.DequeueOrphanedChildrenAsync();
Assert.Equal(0, dequeued);
Assert.Equal(TaskStatus.Queued, _ctx.Tasks.AsNoTracking().Single(t => t.Id == child.Id).Status);
}
[Fact]
public async Task Dequeue_Leaves_Idle_Children_Of_NonPlanning_Parent_Alone()
{