From fe73f45b74f60006de81e68340143caad39c6359 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 9 Jun 2026 23:41:12 +0200 Subject: [PATCH] =?UTF-8?q?fix(worker):=20document=20and=20test=20Queued?= =?UTF-8?q?=E2=86=92Failed=20guard=20in=20FailAsync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OverrideSlotService dispatches RunAsync before calling StartRunningAsync, so a preflight failure (list not found, worktree setup) can reach MarkFailed while the task is still Queued. The guard is intentional, not dead code. - Add comment in FailAsync explaining the OverrideSlotService preflight gap - Add FailAsync_FromQueued_TransitionsToFailed test - Update CLAUDE.md transition table with the precise rationale Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Worker/CLAUDE.md | 2 +- src/ClaudeDo.Worker/State/TaskStateService.cs | 3 +++ .../State/TaskStateServiceTests.cs | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ClaudeDo.Worker/CLAUDE.md b/src/ClaudeDo.Worker/CLAUDE.md index 029c524..532e148 100644 --- a/src/ClaudeDo.Worker/CLAUDE.md +++ b/src/ClaudeDo.Worker/CLAUDE.md @@ -69,7 +69,7 @@ Allowed transitions (enforced by `TaskStateService`): ``` 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) | WaitingForChildren (parent with pending children) | Done (planning/improvement child success) | Failed | Cancelled diff --git a/src/ClaudeDo.Worker/State/TaskStateService.cs b/src/ClaudeDo.Worker/State/TaskStateService.cs index 8f0f445..d616219 100644 --- a/src/ClaudeDo.Worker/State/TaskStateService.cs +++ b/src/ClaudeDo.Worker/State/TaskStateService.cs @@ -198,6 +198,9 @@ public sealed class TaskStateService : ITaskStateService { 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 .Where(t => t.Id == taskId && (t.Status == TaskStatus.Running || t.Status == TaskStatus.Queued)) diff --git a/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs b/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs index 531a6e3..41f06f2 100644 --- a/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs @@ -232,6 +232,21 @@ public sealed class TaskStateServiceTests : IDisposable 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] public async Task FailAsync_FromDone_Rejects() {