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:
committed by
mika kuns
co-authored by
Claude Opus 5
parent
50b6b56f87
commit
c874bf7e52
@@ -138,6 +138,74 @@ public sealed class QueuePickerTests : IDisposable
|
||||
Assert.Equal(dependent.Id, picked!.Id);
|
||||
}
|
||||
|
||||
private async Task SeedWorktreeAsync(string taskId, WorktreeState state)
|
||||
{
|
||||
_ctx.Worktrees.Add(new WorktreeEntity
|
||||
{
|
||||
TaskId = taskId,
|
||||
Path = $"/tmp/{taskId}",
|
||||
BranchName = $"claudedo/{taskId}",
|
||||
BaseCommit = "abc123",
|
||||
State = state,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
await _ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClaimNextAsync_Claims_TaskWhoseDependsOnIsMergedButAwaitingBatchVerify()
|
||||
{
|
||||
// review_task(skipVerify: true) leaves a successfully merged task in WaitingForReview
|
||||
// until verify_merges runs once for the whole batch. Gating on Done alone stalled every
|
||||
// successor for the entire batch, so a landed merge satisfies the edge.
|
||||
var listId = await CreateListAsync();
|
||||
var dependency = await SeedAsync(listId, status: TaskStatus.WaitingForReview);
|
||||
await SeedWorktreeAsync(dependency.Id, WorktreeState.Merged);
|
||||
var dependent = await SeedAsync(listId, dependsOn: [dependency.Id]);
|
||||
|
||||
var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(picked);
|
||||
Assert.Equal(dependent.Id, picked!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClaimNextAsync_Skips_TaskWhoseDependsOnWasRejected()
|
||||
{
|
||||
// reject_rerun/reject_park never merge, so the worktree stays Active -- the successor
|
||||
// must keep waiting even though the predecessor left WaitingForReview.
|
||||
var listId = await CreateListAsync();
|
||||
var dependency = await SeedAsync(listId, status: TaskStatus.Idle);
|
||||
await SeedWorktreeAsync(dependency.Id, WorktreeState.Active);
|
||||
await SeedAsync(listId, dependsOn: [dependency.Id]);
|
||||
|
||||
Assert.Null(await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClaimNextAsync_Skips_TaskWhoseDependsOnMergeWasReverted()
|
||||
{
|
||||
// revert_merge / abort_merge put the worktree back to Active -- the edge is unmet again.
|
||||
var listId = await CreateListAsync();
|
||||
var dependency = await SeedAsync(listId, status: TaskStatus.WaitingForReview);
|
||||
await SeedWorktreeAsync(dependency.Id, WorktreeState.Active);
|
||||
await SeedAsync(listId, dependsOn: [dependency.Id]);
|
||||
|
||||
Assert.Null(await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClaimNextAsync_Skips_TaskWhoseWorktreelessDependsOnIsNotDone()
|
||||
{
|
||||
// A handler task or an empty-diff run has no worktrees row at all: only 'done' can
|
||||
// satisfy that edge, and the merged-branch must not accidentally match a missing row.
|
||||
var listId = await CreateListAsync();
|
||||
var dependency = await SeedAsync(listId, status: TaskStatus.WaitingForReview);
|
||||
await SeedAsync(listId, dependsOn: [dependency.Id]);
|
||||
|
||||
Assert.Null(await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClaimNextAsync_Skips_TaskWhoseDependsOnFailed_ButKeepsClaimingOthers()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user