- Add SubtaskDiffDto and CombinedDiffResultDto to PlanningDtos.cs - Extend IWorkerClient with 5 planning merge methods and 5 events - Implement methods and hub subscriptions on WorkerClient - Add Status and WorktreeState to SubtaskRowViewModel - Add MergeTargetBranches, SelectedMergeTarget, CanMergeAll, MergeAllDisabledReason, MergeAllError, RecomputeCanMergeAll, MergeAllCommand, ReviewCombinedDiffCommand (Task 14 TODO) to DetailsIslandViewModel - Add planning merge section to DetailsIslandView.axaml (merge target ComboBox + buttons + error label), gated on Task.IsPlanningParent - Add 4 xUnit tests covering CanMergeAll logic and DTO shape Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
6.1 KiB
C#
135 lines
6.1 KiB
C#
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<SubtaskRowViewModel> 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<string>? TaskUpdatedEvent;
|
|
public event Action<string>? WorktreeUpdatedEvent;
|
|
public event Action<string, string>? TaskMessageEvent;
|
|
public event Action<string, string>? PlanningMergeStartedEvent;
|
|
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
|
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
|
public event Action<string>? PlanningMergeAbortedEvent;
|
|
public event Action<string>? 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<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
|
|
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) => Task.FromResult(MergeTargetsResult);
|
|
public Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId) =>
|
|
Task.FromResult<IReadOnlyList<SubtaskDiffDto>>(Array.Empty<SubtaskDiffDto>());
|
|
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) =>
|
|
Task.FromResult<CombinedDiffResultDto?>(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);
|
|
}
|
|
}
|