feat(state): add SubmitForChildrenAsync (Running -> WaitingForChildren)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 15:38:15 +02:00
parent f25c7599bd
commit 6f4b5d5544
3 changed files with 147 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Tests.Infrastructure;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using Xunit;
namespace ClaudeDo.Worker.Tests;
public sealed class WaitingForChildrenLifecycleTests : IDisposable
{
private readonly DbFixture _db = new();
private readonly TestDbContextFactory _factory;
private readonly TaskStateServiceBuilder.Built _built;
public WaitingForChildrenLifecycleTests()
{
_factory = _db.CreateFactory();
_built = TaskStateServiceBuilder.Build(_factory);
}
public void Dispose() => _db.Dispose();
private async Task<string> SeedRunningStandaloneAsync()
{
using var ctx = _db.CreateContext();
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", CreatedAt = DateTime.UtcNow });
ctx.Tasks.Add(new TaskEntity { Id = "p1", ListId = "l1", Title = "Parent",
Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
await ctx.SaveChangesAsync();
return "p1";
}
[Fact]
public async Task SubmitForChildren_moves_running_task_to_WaitingForChildren()
{
var id = await SeedRunningStandaloneAsync();
var result = await _built.State.SubmitForChildrenAsync(id, DateTime.UtcNow, "ran ok", default);
Assert.True(result.Ok);
using var ctx = _db.CreateContext();
var t = await new TaskRepository(ctx).GetByIdAsync(id);
Assert.Equal(TaskStatus.WaitingForChildren, t!.Status);
Assert.Equal("ran ok", t.Result);
Assert.NotNull(t.FinishedAt);
}
[Fact]
public async Task SubmitForChildren_rejects_when_not_running()
{
var id = await SeedRunningStandaloneAsync();
await _built.State.SubmitForChildrenAsync(id, DateTime.UtcNow, null, default);
var second = await _built.State.SubmitForChildrenAsync(id, DateTime.UtcNow, null, default);
Assert.False(second.Ok);
}
private async Task SeedParentWithChildrenAsync(string parentStatus, params TaskStatus[] childStatuses)
{
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 = "par", ListId = "l1", Title = "Parent",
Status = Enum.Parse<TaskStatus>(parentStatus), Result = "parent ran", CreatedAt = DateTime.UtcNow });
int i = 0;
foreach (var cs in childStatuses)
ctx.Tasks.Add(new TaskEntity { Id = $"c{i++}", ListId = "l1", Title = "Child",
Status = cs, ParentTaskId = "par", CreatedAt = DateTime.UtcNow });
await ctx.SaveChangesAsync();
}
[Fact]
public async Task LastChildDone_advances_WaitingForChildren_parent_to_WaitingForReview()
{
await SeedParentWithChildrenAsync("WaitingForChildren", TaskStatus.Done, TaskStatus.Running);
await _built.State.CompleteAsync("c1", DateTime.UtcNow, "child ok", default);
using var ctx = _db.CreateContext();
var parent = await new TaskRepository(ctx).GetByIdAsync("par");
Assert.Equal(TaskStatus.WaitingForReview, parent!.Status);
}
[Fact]
public async Task NonLastChild_leaves_parent_in_WaitingForChildren()
{
await SeedParentWithChildrenAsync("WaitingForChildren", TaskStatus.Running, TaskStatus.Idle);
await _built.State.CompleteAsync("c0", DateTime.UtcNow, "ok", default);
using var ctx = _db.CreateContext();
var parent = await new TaskRepository(ctx).GetByIdAsync("par");
Assert.Equal(TaskStatus.WaitingForChildren, parent!.Status);
}
[Fact]
public async Task FailedChild_still_advances_parent_and_flags_failure()
{
await SeedParentWithChildrenAsync("WaitingForChildren", TaskStatus.Done, TaskStatus.Running);
await _built.State.FailAsync("c1", DateTime.UtcNow, "boom", default);
using var ctx = _db.CreateContext();
var parent = await new TaskRepository(ctx).GetByIdAsync("par");
Assert.Equal(TaskStatus.WaitingForReview, parent!.Status);
Assert.Contains("1 failed", parent.Result!);
}
private async Task SeedParentChildPairAsync(PlanningPhase parentPhase)
{
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 = "par", ListId = "l1", Title = "Parent",
Status = TaskStatus.WaitingForChildren, PlanningPhase = parentPhase, CreatedAt = DateTime.UtcNow });
ctx.Tasks.Add(new TaskEntity { Id = "kid", ListId = "l1", Title = "Child",
Status = TaskStatus.Idle, ParentTaskId = "par", CreatedAt = DateTime.UtcNow });
await ctx.SaveChangesAsync();
}
[Fact]
public async Task ImprovementChild_can_be_enqueued()
{
await SeedParentChildPairAsync(PlanningPhase.None);
var result = await _built.State.EnqueueAsync("kid", default);
Assert.True(result.Ok);
using var ctx = _db.CreateContext();
Assert.Equal(TaskStatus.Queued, (await new TaskRepository(ctx).GetByIdAsync("kid"))!.Status);
}
[Fact]
public async Task PlanningDraftChild_cannot_be_enqueued()
{
await SeedParentChildPairAsync(PlanningPhase.Active);
var result = await _built.State.EnqueueAsync("kid", default);
Assert.False(result.Ok);
}
}