using System.ComponentModel; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Planning; namespace ClaudeDo.Ui.Tests.ViewModels; public class PlanningDiffViewModelTests { private sealed class FakePlanningWorker : IWorkerClient { public event PropertyChangedEventHandler? PropertyChanged; public event Action? TaskStartedEvent; public event Action? TaskFinishedEvent; public event Action? TaskUpdatedEvent; public event Action? WorktreeUpdatedEvent; public event Action? TaskMessageEvent; public event Action? PlanningMergeStartedEvent; public event Action? PlanningSubtaskMergedEvent; public event Action>? PlanningMergeConflictEvent; public event Action? PlanningMergeAbortedEvent; public event Action? PlanningCompletedEvent; public bool IsConnected => false; public IReadOnlyList AggregateResult { get; set; } = Array.Empty(); public CombinedDiffResultDto? CombinedResult { get; set; } public Task WakeQueueAsync() => Task.CompletedTask; public Task RunNowAsync(string taskId) => Task.CompletedTask; public Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask; public Task ResetTaskAsync(string taskId) => Task.CompletedTask; public Task CancelTaskAsync(string taskId) => Task.CompletedTask; public Task> GetAgentsAsync() => Task.FromResult(new List()); public Task GetListConfigAsync(string listId) => Task.FromResult(null); public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask; public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => Task.CompletedTask; public Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0); public Task GetMergeTargetsAsync(string taskId) => Task.FromResult(null); public Task> GetPlanningAggregateAsync(string planningTaskId) => Task.FromResult(AggregateResult); public Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult(CombinedResult); public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; } [Fact] public async Task InitializeAsync_PopulatesSubtasks() { var fake = new FakePlanningWorker { AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "branch-1", "base1", "head1", "+1 -0", "diff1"), new SubtaskDiffDto("s2", "Second", "branch-2", "base2", "head2", "+2 -1", "diff2"), } }; var vm = new PlanningDiffViewModel(fake, "plan-1", "main"); await vm.InitializeAsync(); Assert.Equal(2, vm.Subtasks.Count); Assert.Equal(vm.Subtasks[0], vm.SelectedSubtask); } [Fact] public async Task SelectingSubtask_InGroupedMode_SetsDisplayedDiff() { var fake = new FakePlanningWorker { AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"), new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, "DIFF-B"), } }; var vm = new PlanningDiffViewModel(fake, "plan-1", "main"); await vm.InitializeAsync(); vm.SelectedSubtask = vm.Subtasks[1]; Assert.Equal("DIFF-B", vm.DisplayedDiff); } [Fact] public async Task ToggleCombined_Success_DisplaysUnifiedDiff() { var fake = new FakePlanningWorker { AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"), }, CombinedResult = new CombinedDiffResultDto(true, "integration-branch", "COMBINED-DIFF", null, null), }; var vm = new PlanningDiffViewModel(fake, "plan-1", "main"); await vm.InitializeAsync(); vm.IsCombinedMode = true; // Wait for the async toggle command to complete var deadline = DateTime.UtcNow.AddSeconds(5); while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10); Assert.Equal("COMBINED-DIFF", vm.DisplayedDiff); Assert.Null(vm.CombinedWarning); } [Fact] public async Task ToggleCombined_Conflict_ShowsWarning() { var fake = new FakePlanningWorker { AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"), }, CombinedResult = new CombinedDiffResultDto(false, null, null, "subtask-42", new[] { "a.cs", "b.cs" }), }; var vm = new PlanningDiffViewModel(fake, "plan-1", "main"); await vm.InitializeAsync(); vm.IsCombinedMode = true; var deadline = DateTime.UtcNow.AddSeconds(5); while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10); Assert.NotNull(vm.CombinedWarning); Assert.Contains("subtask-42", vm.CombinedWarning); Assert.Contains("2 files", vm.CombinedWarning); } [Fact] public async Task ToggleCombined_HubReturnsNull_ShowsError() { var fake = new FakePlanningWorker { AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"), }, CombinedResult = null, }; var vm = new PlanningDiffViewModel(fake, "plan-1", "main"); await vm.InitializeAsync(); vm.IsCombinedMode = true; var deadline = DateTime.UtcNow.AddSeconds(5); while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10); Assert.NotNull(vm.CombinedWarning); Assert.NotEmpty(vm.CombinedWarning!); } }