refactor(worker): drop ForceSetStatusAsync, route every status write through a guarded transition

Mark-done now fires OnChildTerminalAsync, so ticking a chain child or the last open child
off by hand unblocks the successor and advances the parent. The hub's Mark-as menu and
update_task_status map onto MarkDone/Cancel(retire)/Enqueue/Reset; revert_merge uses the
new ReopenForReviewAsync (Done|WaitingForReview -> WaitingForReview).
This commit is contained in:
mika kuns
2026-10-02 16:03:59 +02:00
parent 59d990c267
commit ff7197c410
9 changed files with 113 additions and 61 deletions
@@ -534,6 +534,58 @@ public sealed class TaskStateServiceTests : IDisposable
Assert.Equal(TaskStatus.Done, await GetStatusAsync(id));
}
[Fact]
public async Task MarkDoneAsync_OnChainChild_UnblocksSuccessor()
{
var parent = await SeedTaskAsync(TaskStatus.WaitingForChildren, phase: PlanningPhase.Finalized);
var c0 = await SeedTaskAsync(TaskStatus.Queued, parentId: parent, sortOrder: 0);
var c1 = await SeedTaskAsync(TaskStatus.Queued, parentId: parent, sortOrder: 1, blockedBy: c0);
await _sut.MarkDoneAsync(c0, DateTime.UtcNow, default);
Assert.Null((await GetTaskAsync(c1)).BlockedByTaskId);
}
[Fact]
public async Task MarkDoneAsync_OnLastOpenChild_AdvancesParentToReview()
{
var parent = await SeedTaskAsync(TaskStatus.WaitingForChildren, phase: PlanningPhase.Finalized);
await SeedTaskAsync(TaskStatus.Done, parentId: parent);
var open = await SeedTaskAsync(TaskStatus.Failed, parentId: parent);
await _sut.MarkDoneAsync(open, DateTime.UtcNow, default);
Assert.Equal(TaskStatus.WaitingForReview, await GetStatusAsync(parent));
}
[Theory]
[InlineData(TaskStatus.Done, true)]
[InlineData(TaskStatus.WaitingForReview, true)]
[InlineData(TaskStatus.Idle, false)]
[InlineData(TaskStatus.Running, false)]
public async Task ReopenForReviewAsync_OnlyFromDoneOrReview(TaskStatus from, bool ok)
{
var id = await SeedTaskAsync(from);
var result = await _sut.ReopenForReviewAsync(id, default);
Assert.Equal(ok, result.Ok);
Assert.Equal(ok ? TaskStatus.WaitingForReview : from, await GetStatusAsync(id));
}
[Theory]
[InlineData(TaskStatus.Idle)]
[InlineData(TaskStatus.Failed)]
public async Task CancelAsync_Retire_AllowsIdleAndFailed(TaskStatus from)
{
var id = await SeedTaskAsync(from);
var result = await _sut.CancelAsync(id, DateTime.UtcNow, default, retire: true);
Assert.True(result.Ok);
Assert.Equal(TaskStatus.Cancelled, await GetStatusAsync(id));
}
[Fact]
public async Task UnmarkDoneAsync_FromDone_TransitionsToIdle()
{