fix(worker): depends_on-Nachfolger schon beim gelandeten Merge freigeben

Der Queue-Picker gatete eine task_dependencies-Kante auf status='done'.
review_task(skipVerify) laesst einen erfolgreich gemergten Task aber
bewusst auf WaitingForReview stehen, bis verify_merges einmal zentral
laeuft -- damit blockierte jeder Nachfolger ueber den gesamten Batch plus
Full-Suite-Verify hinweg, bei freien Slots.

Eine Kante gilt jetzt als erfuellt, wenn der Vorgaenger 'done' ist ODER
sein Merge gelandet ist (worktrees.state = 'merged'). TaskBlocking
(get_task/list_tasks/wait_for_task_change) und der UI-Wartehinweis folgen
derselben Regel, damit "blocked" nicht meldet, was der Picker claimt.

Abgeleitet: revert_merge/abort_merge setzen den Worktree auf Active und
blockieren wieder; reject_rerun/reject_park haben nie gemergt und bleiben
unerfuellt; ein Task ohne Worktree wird nur ueber 'done' erfuellt; ein
fehlgeschlagener verify_merges gibt Nachfolger frei, weil der Code so
oder so auf main liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-09-16 15:43:37 +02:00
committed by mika kuns
co-authored by Claude Opus 5
parent 50b6b56f87
commit c874bf7e52
8 changed files with 159 additions and 31 deletions
@@ -472,6 +472,34 @@ public sealed class ExternalMcpServiceTests : IDisposable
Assert.Null(dto.BlockedReason);
}
[Fact]
public async Task GetTask_QueuedWithMergedButUnverifiedDependsOn_ReportsBlockedFalse()
{
// Same rule as QueuePicker: a predecessor merged with skipVerify sits in
// WaitingForReview until the batch verify, but the picker already claims the successor --
// get_task must not contradict that by still calling it blocked.
var listId = await SeedListAsync();
var predecessor = await SeedTaskAsync(listId, status: TaskStatus.WaitingForReview);
_ctx.Worktrees.Add(new WorktreeEntity
{
TaskId = predecessor.Id,
Path = $"/tmp/{predecessor.Id}",
BranchName = $"claudedo/{predecessor.Id}",
BaseCommit = "abc123",
State = WorktreeState.Merged,
CreatedAt = DateTime.UtcNow,
});
await _ctx.SaveChangesAsync();
var task = await SeedTaskAsync(listId, status: TaskStatus.Queued);
await _ctx.SeedDependsOnAsync(task.Id, predecessor.Id);
var sut = BuildSut(CreateQueue());
var dto = await sut.GetTask(task.Id, CancellationToken.None);
Assert.False(dto.Blocked);
Assert.Null(dto.BlockedReason);
}
[Fact]
public async Task GetTask_IdleWithDependsOnTaskId_ReportsBlockedFalse_NotYetQueued()
{