feat(worker): planning finalize enters WaitingForChildren
A finalized planning parent now joins the unified parent lifecycle: WaitingForChildren while its child chain runs (or WaitingForReview directly if it has no children), advancing to review like an improvement parent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -289,12 +289,16 @@ public sealed class TaskStateService : ITaskStateService
|
|||||||
public async Task<TransitionResult> FinalizePlanningAsync(string parentId, CancellationToken ct)
|
public async Task<TransitionResult> FinalizePlanningAsync(string parentId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var hasChildren = await ctx.Tasks.AnyAsync(t => t.ParentTaskId == parentId, ct);
|
||||||
|
var newStatus = hasChildren ? TaskStatus.WaitingForChildren : TaskStatus.WaitingForReview;
|
||||||
|
|
||||||
var affected = await ctx.Tasks
|
var affected = await ctx.Tasks
|
||||||
.Where(t => t.Id == parentId && t.PlanningPhase == PlanningPhase.Active)
|
.Where(t => t.Id == parentId && t.PlanningPhase == PlanningPhase.Active)
|
||||||
.ExecuteUpdateAsync(s => s
|
.ExecuteUpdateAsync(s => s
|
||||||
.SetProperty(t => t.PlanningPhase, PlanningPhase.Finalized)
|
.SetProperty(t => t.PlanningPhase, PlanningPhase.Finalized)
|
||||||
.SetProperty(t => t.PlanningFinalizedAt, DateTime.UtcNow)
|
.SetProperty(t => t.PlanningFinalizedAt, DateTime.UtcNow)
|
||||||
.SetProperty(t => t.PlanningSessionToken, (string?)null), ct);
|
.SetProperty(t => t.PlanningSessionToken, (string?)null)
|
||||||
|
.SetProperty(t => t.Status, newStatus), ct);
|
||||||
|
|
||||||
if (affected == 0)
|
if (affected == 0)
|
||||||
return new TransitionResult(false, "No active planning session.");
|
return new TransitionResult(false, "No active planning session.");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using ClaudeDo.Data;
|
|||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -160,4 +161,95 @@ public sealed class WaitingForChildrenLifecycleTests : IDisposable
|
|||||||
Assert.Equal(TaskStatus.WaitingForReview, par!.Status);
|
Assert.Equal(TaskStatus.WaitingForReview, par!.Status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── FinalizePlanningAsync ────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task<string> SeedActivePlanningParentAsync(string id = "par")
|
||||||
|
{
|
||||||
|
using var ctx = _db.CreateContext();
|
||||||
|
if (!ctx.Lists.Any())
|
||||||
|
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", CreatedAt = DateTime.UtcNow });
|
||||||
|
ctx.Tasks.Add(new TaskEntity
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
ListId = "l1",
|
||||||
|
Title = "Parent",
|
||||||
|
Status = TaskStatus.Idle,
|
||||||
|
PlanningPhase = PlanningPhase.Active,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedChildAsync(string parentId, string childId, TaskStatus status = TaskStatus.Idle)
|
||||||
|
{
|
||||||
|
using var ctx = _db.CreateContext();
|
||||||
|
ctx.Tasks.Add(new TaskEntity
|
||||||
|
{
|
||||||
|
Id = childId,
|
||||||
|
ListId = "l1",
|
||||||
|
Title = "Child",
|
||||||
|
Status = status,
|
||||||
|
ParentTaskId = parentId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizePlanning_WithChildren_SetsWaitingForChildren_AndPlanningFinalized()
|
||||||
|
{
|
||||||
|
await SeedActivePlanningParentAsync();
|
||||||
|
await SeedChildAsync("par", "c1");
|
||||||
|
await SeedChildAsync("par", "c2");
|
||||||
|
|
||||||
|
var result = await _built.State.FinalizePlanningAsync("par", default);
|
||||||
|
|
||||||
|
Assert.True(result.Ok);
|
||||||
|
using var ctx = _db.CreateContext();
|
||||||
|
var parent = await new TaskRepository(ctx).GetByIdAsync("par");
|
||||||
|
Assert.Equal(TaskStatus.WaitingForChildren, parent!.Status);
|
||||||
|
Assert.Equal(PlanningPhase.Finalized, parent.PlanningPhase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizePlanning_WithChildren_ThenAllChildrenTerminal_AdvancesToWaitingForReview()
|
||||||
|
{
|
||||||
|
await SeedActivePlanningParentAsync();
|
||||||
|
await SeedChildAsync("par", "c1");
|
||||||
|
await SeedChildAsync("par", "c2");
|
||||||
|
|
||||||
|
await _built.State.FinalizePlanningAsync("par", default);
|
||||||
|
|
||||||
|
// Manually transition parent to WaitingForChildren so CompleteAsync works on the children.
|
||||||
|
// Children are Idle — complete them by seeding them as Running first.
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
await ctx.Tasks.Where(t => t.Id == "c1").ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, TaskStatus.Running));
|
||||||
|
await ctx.Tasks.Where(t => t.Id == "c2").ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, TaskStatus.Running));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _built.State.CompleteAsync("c1", DateTime.UtcNow, "ok", default);
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
Assert.Equal(TaskStatus.WaitingForChildren, (await new TaskRepository(ctx).GetByIdAsync("par"))!.Status);
|
||||||
|
|
||||||
|
await _built.State.CompleteAsync("c2", DateTime.UtcNow, "ok", default);
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
Assert.Equal(TaskStatus.WaitingForReview, (await new TaskRepository(ctx).GetByIdAsync("par"))!.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizePlanning_WithNoChildren_SetsWaitingForReview_Directly()
|
||||||
|
{
|
||||||
|
await SeedActivePlanningParentAsync();
|
||||||
|
|
||||||
|
var result = await _built.State.FinalizePlanningAsync("par", default);
|
||||||
|
|
||||||
|
Assert.True(result.Ok);
|
||||||
|
using var ctx = _db.CreateContext();
|
||||||
|
var parent = await new TaskRepository(ctx).GetByIdAsync("par");
|
||||||
|
Assert.Equal(TaskStatus.WaitingForReview, parent!.Status);
|
||||||
|
Assert.Equal(PlanningPhase.Finalized, parent.PlanningPhase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user