using System.Collections.ObjectModel; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Islands; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.Tests.ViewModels; public class DetailsIslandPlanningTests { // Minimal shim exercising RecomputeCanMergeAll logic without needing WorkerClient private sealed class PlanningVmShim { public ObservableCollection Subtasks { get; } = new(); public bool CanMergeAll { get; private set; } public string? MergeAllDisabledReason { get; private set; } public void RecomputeCanMergeAll() { var notDone = Subtasks.Count(c => c.Status != TaskStatus.Done); if (notDone > 0) { CanMergeAll = false; MergeAllDisabledReason = $"{notDone} subtask(s) not done"; return; } var badWt = Subtasks.FirstOrDefault(c => c.WorktreeState == WorktreeState.Discarded || c.WorktreeState == WorktreeState.Kept); if (badWt is not null) { CanMergeAll = false; MergeAllDisabledReason = "at least one worktree was discarded/kept"; return; } CanMergeAll = true; MergeAllDisabledReason = null; } } private sealed class FakeWorkerClient : IWorkerClient { 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 MergeTargetsDto? MergeTargetsResult { get; set; } public Task WakeQueueAsync() => 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(MergeTargetsResult); public Task> GetPlanningAggregateAsync(string planningTaskId) => Task.FromResult>(Array.Empty()); public Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult(null); public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public void FireTaskUpdated(string id) => TaskUpdatedEvent?.Invoke(id); public void FireWorktreeUpdated(string id) => WorktreeUpdatedEvent?.Invoke(id); } private static SubtaskRowViewModel MakeSubtask(TaskStatus status, WorktreeState wt = WorktreeState.Active) => new() { Id = Guid.NewGuid().ToString(), Title = "t", Status = status, WorktreeState = wt }; [Fact] public void CanMergeAll_AllChildrenDoneActiveWorktrees_True() { var shim = new PlanningVmShim(); shim.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); shim.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); shim.RecomputeCanMergeAll(); Assert.True(shim.CanMergeAll); Assert.Null(shim.MergeAllDisabledReason); } [Fact] public void CanMergeAll_AnyChildNotDone_FalseWithReason() { var shim = new PlanningVmShim(); shim.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); shim.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); shim.Subtasks.Add(MakeSubtask(TaskStatus.Running, WorktreeState.Active)); shim.RecomputeCanMergeAll(); Assert.False(shim.CanMergeAll); Assert.NotNull(shim.MergeAllDisabledReason); Assert.Contains("1 subtask", shim.MergeAllDisabledReason, StringComparison.OrdinalIgnoreCase); Assert.Contains("not done", shim.MergeAllDisabledReason, StringComparison.OrdinalIgnoreCase); } [Fact] public void CanMergeAll_AnyChildDiscarded_FalseWithReason() { var shim = new PlanningVmShim(); shim.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); shim.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Discarded)); shim.RecomputeCanMergeAll(); Assert.False(shim.CanMergeAll); Assert.NotNull(shim.MergeAllDisabledReason); Assert.True( shim.MergeAllDisabledReason!.Contains("discarded", StringComparison.OrdinalIgnoreCase) || shim.MergeAllDisabledReason.Contains("kept", StringComparison.OrdinalIgnoreCase)); } [Fact] public void MergeTargetBranches_LoadedFromWorkerOnPlanningParent() { var fake = new FakeWorkerClient(); fake.MergeTargetsResult = new MergeTargetsDto("main", new[] { "main", "dev" }); var result = fake.GetMergeTargetsAsync("any-task-id").GetAwaiter().GetResult(); Assert.NotNull(result); Assert.Equal("main", result!.DefaultBranch); Assert.Contains("main", result.LocalBranches); Assert.Contains("dev", result.LocalBranches); } }