feat(worker): surface rebase and worktree-maintenance progress on OperationProgress

RebaseOthersAfterMergeAsync now broadcasts a "rebasing" phase with i/n over the
WaitingForReview branches it checks, so the merge/continue_merge callers stop
showing the stalled "Merging…" phase while the best-effort rebase loop runs
(rebase still runs before the verify gate; a failed rebase still leaves the
merge itself successful). WorktreeMaintenanceService gained an optional
HubBroadcaster to report the same i/n shape per worktree during
cleanup/reset, with no new UI surface (deliberately out of scope). Both
review-action viewmodels now also listen on OperationProgressEvent (which
carries the total that the elapsed-seconds-only MergeProgressEvent drops) to
render "Rebasing other worktrees… (i/n)".
This commit is contained in:
mika kuns
2026-08-21 13:33:02 +02:00
parent dcda067b48
commit 42de97e16a
8 changed files with 183 additions and 9 deletions
@@ -255,6 +255,40 @@ public class DetailsIslandReviewActionsTests : IDisposable
Assert.Equal(0, MergeProgressSubscriberCount(worker));
}
private static int OperationProgressSubscriberCount(StubWorkerClient worker)
{
var field = typeof(StubWorkerClient).GetField("OperationProgressEvent",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var del = (Delegate?)field!.GetValue(worker);
return del?.GetInvocationList().Length ?? 0;
}
[Fact]
public async Task ApproveReview_ShowsRebasePhase_WithBranchCount_InsteadOfStalledMerging()
{
var worker = new BlockingApproveWorkerClient();
var vm = BuildVm(worker);
vm.Bind(new TaskRowViewModel { Id = "task-approve-rebase", Status = TaskStatus.WaitingForReview });
vm.Monitor.ApplyState(TaskStatus.WaitingForReview);
var approve = vm.ApproveReviewCommand.ExecuteAsync(null);
var merging = vm.ApproveOp.Label;
// The rebase phase needs the branch total, which only rides OperationProgress
// (MergeProgressEvent forwards elapsed-seconds only) — ignored for other tasks.
worker.RaiseOperationProgress("some-other-task", "rebasing", 1, 3);
Assert.Equal(merging, vm.ApproveOp.Label);
worker.RaiseOperationProgress("task-approve-rebase", "rebasing", 2, 3);
Assert.NotEqual(merging, vm.ApproveOp.Label);
Assert.Contains("(2/3)", vm.ApproveOp.Label);
worker.Gate.SetResult(new MergeResultDto("merged", new List<string>(), null));
await approve;
Assert.Equal(0, OperationProgressSubscriberCount(worker));
}
private sealed class BlockingSubmitWorkerClient : StubWorkerClient
{
public override bool IsConnected => true;
@@ -119,4 +119,26 @@ public class MergeModalViewModelTests
Assert.Null(vm.ProgressMessage);
}
[Fact]
public async Task Submit_shows_the_rebase_phase_with_branch_count_instead_of_stalled_merging()
{
var (vm, worker) = Build();
await vm.InitializeAsync("task-1", "do the thing");
worker.BlockMerge = true;
var submit = vm.SubmitCommand.ExecuteAsync(null);
var merging = vm.ProgressMessage;
// Needs the branch total, which only rides OperationProgress (MergeProgressEvent
// forwards elapsed-seconds only) -- and must not be clobbered by that other forwarder.
worker.RaiseOperationProgress("task-1", "rebasing", 1, 2);
Assert.NotEqual(merging, vm.ProgressMessage);
Assert.Contains("(1/2)", vm.ProgressMessage);
worker.MergeGate.SetResult(new MergeResultDto("merged", new List<string>(), null));
await submit;
Assert.Null(vm.ProgressMessage);
}
}