fix(worker): route done-toggle and dequeue through guarded TaskStateService transitions

The task-list done toggle (both islands) and RemoveFromQueue wrote TaskEntity.Status
directly via EF, bypassing TaskStateService: no TaskUpdated broadcast, no guard against
a concurrent picker claim (lost update), and no status-based filter. Added guarded
MarkDoneAsync/UnmarkDoneAsync/DequeueToIdleAsync transitions plus matching hub methods
(SetTaskDone/UnsetTaskDone/DequeueTask) and IWorkerClient wrappers; the three UI call
sites now route through the hub with optimistic-then-revert row updates and
ErrorReported on failure. RemoveFromQueueAsync dequeues each queued child individually
through the same guarded path instead of cascading via a raw EF update.

Also closes two hub guard gaps: UpdateListConfig's delete branch now preserves a list's
SerializeOnFileOverlap flag instead of dropping it, and SubmitTaskForReview's Idle/Failed
status gate now runs before either mutation branch so a Done/Cancelled task can't get
committed or stamped and then rejected.
This commit is contained in:
mika kuns
2026-08-21 11:47:24 +02:00
parent ce98f65ca5
commit 7f337b36c7
20 changed files with 806 additions and 49 deletions
@@ -96,6 +96,16 @@ public interface IWorkerClient : INotifyPropertyChanged
/// a list whose working dir has uncommitted changes (null otherwise) — a new worktree forks
/// from the last commit, not the working tree, so those changes won't be included.</summary>
Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, TaskStatus status);
/// <summary>Guarded "manual done toggle" (checks the task is currently Idle server-side
/// before flipping it to Done) — replaces a raw EF write in the task-list checkbox path.</summary>
Task SetTaskDoneAsync(string taskId);
/// <summary>Guarded un-toggle (checks the task is currently Done server-side before
/// flipping it back to Idle).</summary>
Task UnsetTaskDoneAsync(string taskId);
/// <summary>Guarded "remove from queue" (checks the task is currently Queued server-side
/// and clears BlockedByTaskId in the same update) — call once per row (parent and each
/// queued child) rather than cascading server-side.</summary>
Task DequeueTaskAsync(string taskId);
Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage);
+9
View File
@@ -536,6 +536,15 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
return result?.BaseDirty;
}
public Task SetTaskDoneAsync(string taskId)
=> InvokeTimedAsync("SetTaskDone", () => _hub.InvokeAsync("SetTaskDone", taskId));
public Task UnsetTaskDoneAsync(string taskId)
=> InvokeTimedAsync("UnsetTaskDone", () => _hub.InvokeAsync("UnsetTaskDone", taskId));
public Task DequeueTaskAsync(string taskId)
=> InvokeTimedAsync("DequeueTask", () => _hub.InvokeAsync("DequeueTask", taskId));
public async Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
{
LastApproveTarget = targetBranch;