Files
ClaudeDo/tests/ClaudeDo.Ui.Tests/ViewModels/MergeModalViewModelTests.cs
T
mika kuns cad0582b37 fix(merge): conventional merge-commit default and live verify progress
The merge commit message was hand-rolled per caller ("Merge task: <title>",
"Merge <branch>", "Merge subtask") and ignored the task's commit type. Every
caller now passes a blank message and TaskMergeService fills in
CommitMessageBuilder.BuildMerge -> {commitType}(list-slug): merge <title> plus the
ClaudeDo-Task trailer; the merge modal prefills it from GetMergeTargets.

A merge whose list has a verify command holds the MergeTask call for minutes (5m46s
on this repo), during which the modal only disabled its button - no spinner, no
message, so a landed merge looked like a dead app. TaskMergeService now broadcasts
MergeProgress(taskId, phase, elapsedSeconds) for the merging and verifying phases
(re-reported every 30s) plus a WorkerLog line when verify starts; the modal shows a
spinner and the localized phase.
2026-08-11 19:14:36 +02:00

123 lines
4.1 KiB
C#

using System.Collections.Generic;
using System.Threading.Tasks;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;
using Xunit;
namespace ClaudeDo.Ui.Tests.ViewModels;
/// <summary>
/// Covers the two things that made a working merge look broken: the prefilled commit message
/// (must be the worker's conventional default, not a hand-rolled "Merge task: …") and the
/// progress feedback while the post-merge verify gate holds the call open for minutes.
/// </summary>
public class MergeModalViewModelTests
{
private sealed class Worker : StubWorkerClient
{
public MergeTargetsDto? Targets { get; set; } =
new("main", new[] { "main", "dev" }, "fix(my-list): merge do the thing");
public TaskCompletionSource<MergeResultDto> MergeGate { get; } = new();
public bool BlockMerge { get; set; }
public string? CapturedCommitMessage { get; private set; }
public override Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) =>
Task.FromResult(Targets);
public override Task<MergeResultDto> MergeTaskAsync(
string taskId, string targetBranch, bool removeWorktree, string commitMessage)
{
CapturedCommitMessage = commitMessage;
return BlockMerge
? MergeGate.Task
: Task.FromResult(new MergeResultDto("merged", new List<string>(), null));
}
}
private static (MergeModalViewModel vm, Worker worker) Build()
{
var worker = new Worker();
return (new MergeModalViewModel(worker, new MergeCoordinator()), worker);
}
[Fact]
public async Task Initialize_prefills_the_workers_default_commit_message()
{
var (vm, _) = Build();
await vm.InitializeAsync("task-1", "do the thing");
Assert.Equal("fix(my-list): merge do the thing", vm.CommitMessage);
Assert.Equal("main", vm.SelectedBranch);
}
[Fact]
public async Task Initialize_keeps_local_fallback_when_worker_sends_no_default()
{
var (vm, worker) = Build();
worker.Targets = new MergeTargetsDto("main", new[] { "main" }, "");
await vm.InitializeAsync("task-1", "do the thing");
Assert.False(string.IsNullOrWhiteSpace(vm.CommitMessage));
Assert.NotEqual("", vm.CommitMessage);
}
[Fact]
public async Task Submit_shows_a_phase_while_the_merge_is_pending_and_clears_it_after()
{
var (vm, worker) = Build();
await vm.InitializeAsync("task-1", "do the thing");
worker.BlockMerge = true;
var submit = vm.SubmitCommand.ExecuteAsync(null);
Assert.True(vm.IsBusy);
var merging = vm.ProgressMessage;
Assert.False(string.IsNullOrWhiteSpace(merging));
// The verify gate starts — a distinct phase, so the user sees why this is taking minutes.
// (The elapsed time rides in a format argument, which the test localizer drops.)
worker.RaiseMergeProgress("task-1", "verifying", 90);
Assert.NotEqual(merging, vm.ProgressMessage);
worker.MergeGate.SetResult(new MergeResultDto("merged", new List<string>(), null));
await submit;
Assert.Null(vm.ProgressMessage);
Assert.False(vm.IsBusy);
Assert.True(vm.Merged);
}
[Fact]
public async Task Progress_for_another_task_is_ignored()
{
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;
worker.RaiseMergeProgress("some-other-task", "verifying", 42);
Assert.Equal(merging, vm.ProgressMessage);
worker.MergeGate.SetResult(new MergeResultDto("merged", new List<string>(), null));
await submit;
}
[Fact]
public async Task Submit_unsubscribes_so_a_later_broadcast_cannot_revive_the_progress_line()
{
var (vm, worker) = Build();
await vm.InitializeAsync("task-1", "do the thing");
await vm.SubmitCommand.ExecuteAsync(null);
worker.RaiseMergeProgress("task-1", "verifying", 10);
Assert.Null(vm.ProgressMessage);
}
}