using System.Collections.Generic; using System.Threading.Tasks; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals; using Xunit; namespace ClaudeDo.Ui.Tests.ViewModels; /// /// 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. /// 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 MergeGate { get; } = new(); public bool BlockMerge { get; set; } public string? CapturedCommitMessage { get; private set; } public override Task GetMergeTargetsAsync(string taskId) => Task.FromResult(Targets); public override Task MergeTaskAsync( string taskId, string targetBranch, bool removeWorktree, string commitMessage) { CapturedCommitMessage = commitMessage; return BlockMerge ? MergeGate.Task : Task.FromResult(new MergeResultDto("merged", new List(), 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(), 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(), 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); } }