Merge task branch for: fix(worker): FailAsync-Guard untersuchen — ist Queued→Failed erreichbar/gewollt?

This commit is contained in:
mika kuns
2026-06-09 23:46:18 +02:00
3 changed files with 19 additions and 1 deletions

View File

@@ -69,7 +69,7 @@ Allowed transitions (enforced by `TaskStateService`):
``` ```
Idle → Queued | Running (RunNow) Idle → Queued | Running (RunNow)
Queued → Running | Cancelled | Idle | Failed (runner guard) Queued → Running | Cancelled | Idle | Failed (OverrideSlotService preflight gap: RunAsync can fail before StartRunningAsync is called)
Running → WaitingForReview (standalone success, no children) Running → WaitingForReview (standalone success, no children)
| WaitingForChildren (parent with pending children) | WaitingForChildren (parent with pending children)
| Done (planning/improvement child success) | Failed | Cancelled | Done (planning/improvement child success) | Failed | Cancelled

View File

@@ -198,6 +198,9 @@ public sealed class TaskStateService : ITaskStateService
{ {
await using (var ctx = await _dbFactory.CreateDbContextAsync(ct)) await using (var ctx = await _dbFactory.CreateDbContextAsync(ct))
{ {
// Queued is intentional: OverrideSlotService dispatches RunAsync before calling
// StartRunningAsync, so a preflight failure (list not found, worktree setup) can
// reach MarkFailed while the task is still Queued in the DB.
var affected = await ctx.Tasks var affected = await ctx.Tasks
.Where(t => t.Id == taskId && .Where(t => t.Id == taskId &&
(t.Status == TaskStatus.Running || t.Status == TaskStatus.Queued)) (t.Status == TaskStatus.Running || t.Status == TaskStatus.Queued))

View File

@@ -232,6 +232,21 @@ public sealed class TaskStateServiceTests : IDisposable
Assert.Equal("boom", t.Result); Assert.Equal("boom", t.Result);
} }
[Fact]
public async Task FailAsync_FromQueued_TransitionsToFailed()
{
// OverrideSlotService can call MarkFailed before StartRunningAsync when a
// preflight step (list lookup, worktree setup) fails — the task is still Queued.
var id = await SeedTaskAsync(TaskStatus.Queued);
var result = await _sut.FailAsync(id, DateTime.UtcNow, "list not found", default);
Assert.True(result.Ok);
var t = await GetTaskAsync(id);
Assert.Equal(TaskStatus.Failed, t.Status);
Assert.Equal("list not found", t.Result);
}
[Fact] [Fact]
public async Task FailAsync_FromDone_Rejects() public async Task FailAsync_FromDone_Rejects()
{ {