feat(planning): consolidate finalize+chain via TaskStateService, fix queue pickup
Slice 4 of the worker state consolidation refactor. Eliminates the "queue never picks up planning tasks" bug structurally by routing both the manager and MCP finalize paths through TaskStateService and PlanningChainCoordinator.SetupChainAsync, where the auto-wake on enqueue guarantees the queue picker claims the first child immediately. - Delete TaskRepository.FinalizePlanningAsync; PlanningSessionManager now orchestrates via _state.FinalizePlanningAsync + _chain.SetupChainAsync. - Rename QueueSubtasksSequentiallyAsync to SetupChainAsync (internal); layout is now Status=Queued + BlockedByTaskId, with auto-attached agent tag. - OnChildFinishedAsync looks up the successor by BlockedByTaskId, drops the legacy Waiting status lookup. - PlanningMcpService.Finalize routes through state+chain; EditableStatuses drops Waiting and adds Idle; gate uses PlanningPhase==Active. - TaskStateService.FinalizePlanningAsync clears the planning session token. - UI: TaskRowViewModel adds BlockedByTaskId; IsQueued/IsWaiting reflect the new layout; TasksIslandViewModel.RemoveFromQueueAsync clears BlockedByTaskId on dequeue. - New regression test PlanningEndToEndTests.FinalizeAsync_FirstChildIs ClaimedByPicker_WithinDeadline asserts the picker claims the first child within 200ms with no manual WakeQueue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,11 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
|
||||
public void Dispose() => _db.Dispose();
|
||||
|
||||
private async Task<string> SeedTaskAsync(TaskStatus status, string? parentId = null, int sortOrder = 0)
|
||||
private async Task<string> SeedTaskAsync(
|
||||
TaskStatus status,
|
||||
string? parentId = null,
|
||||
int sortOrder = 0,
|
||||
string? blockedBy = null)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
await using var ctx = _factory.CreateDbContext();
|
||||
@@ -49,6 +53,7 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ParentTaskId = parentId,
|
||||
SortOrder = sortOrder,
|
||||
BlockedByTaskId = blockedBy,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
return id;
|
||||
@@ -364,20 +369,24 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
// ─── Child terminal → chain advance ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task CompleteAsync_OnChild_AdvancesNextWaitingSibling()
|
||||
public async Task CompleteAsync_OnChild_AdvancesNextBlockedSibling()
|
||||
{
|
||||
var parent = await SeedTaskAsync(TaskStatus.Planned);
|
||||
var c0 = await SeedTaskAsync(TaskStatus.Running, parentId: parent, sortOrder: 0);
|
||||
var c1 = await SeedTaskAsync(TaskStatus.Waiting, parentId: parent, sortOrder: 1);
|
||||
var c2 = await SeedTaskAsync(TaskStatus.Waiting, parentId: parent, sortOrder: 2);
|
||||
var c1 = await SeedTaskAsync(TaskStatus.Queued, parentId: parent, sortOrder: 1, blockedBy: c0);
|
||||
var c2 = await SeedTaskAsync(TaskStatus.Queued, parentId: parent, sortOrder: 2, blockedBy: c1);
|
||||
|
||||
var result = await _sut.CompleteAsync(c0, DateTime.UtcNow, "ok", default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.Equal(TaskStatus.Done, await GetStatusAsync(c0));
|
||||
// Next sibling was Waiting → chain coordinator unblocks → Queued.
|
||||
Assert.Equal(TaskStatus.Queued, await GetStatusAsync(c1));
|
||||
// Subsequent sibling untouched.
|
||||
Assert.Equal(TaskStatus.Waiting, await GetStatusAsync(c2));
|
||||
// c1 was BlockedBy=c0 → chain coordinator unblocks → BlockedByTaskId cleared, still Queued.
|
||||
var t1 = await GetTaskAsync(c1);
|
||||
Assert.Equal(TaskStatus.Queued, t1.Status);
|
||||
Assert.Null(t1.BlockedByTaskId);
|
||||
// c2 still blocked on c1.
|
||||
var t2 = await GetTaskAsync(c2);
|
||||
Assert.Equal(TaskStatus.Queued, t2.Status);
|
||||
Assert.Equal(c1, t2.BlockedByTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user