feat(ui): add merge-target dropdown and merge-all controls to planning detail
- 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>
This commit is contained in:
@@ -6,10 +6,22 @@ public interface IWorkerClient
|
||||
event Action<string>? WorktreeUpdatedEvent;
|
||||
event Action<string, string>? TaskMessageEvent;
|
||||
|
||||
event Action<string, string>? PlanningMergeStartedEvent;
|
||||
event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||
event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
||||
event Action<string>? PlanningMergeAbortedEvent;
|
||||
event Action<string>? PlanningCompletedEvent;
|
||||
|
||||
Task WakeQueueAsync();
|
||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
||||
Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default);
|
||||
Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId);
|
||||
Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId);
|
||||
Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch);
|
||||
Task MergeAllPlanningAsync(string planningTaskId, string targetBranch);
|
||||
Task ContinuePlanningMergeAsync(string planningTaskId);
|
||||
Task AbortPlanningMergeAsync(string planningTaskId);
|
||||
}
|
||||
|
||||
@@ -16,3 +16,19 @@ public sealed record PlanningSessionResumeInfo(
|
||||
string WorkingDir,
|
||||
string ClaudeSessionId,
|
||||
string McpConfigPath);
|
||||
|
||||
public sealed record SubtaskDiffDto(
|
||||
string SubtaskId,
|
||||
string Title,
|
||||
string BranchName,
|
||||
string BaseCommit,
|
||||
string HeadCommit,
|
||||
string? DiffStat,
|
||||
string UnifiedDiff);
|
||||
|
||||
public sealed record CombinedDiffResultDto(
|
||||
bool Success,
|
||||
string? IntegrationBranch,
|
||||
string? UnifiedDiff,
|
||||
string? FirstConflictSubtaskId,
|
||||
IReadOnlyList<string>? ConflictedFiles);
|
||||
|
||||
@@ -49,6 +49,12 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public event Action<string>? ListUpdatedEvent;
|
||||
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||
|
||||
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 WorkerClient(string signalRUrl)
|
||||
{
|
||||
_hub = new HubConnectionBuilder()
|
||||
@@ -123,6 +129,31 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
{
|
||||
WorkerLogReceivedEvent?.Invoke(new WorkerLogEntry(message, level, timestampUtc));
|
||||
});
|
||||
|
||||
_hub.On<string, string>("PlanningMergeStarted", (planningTaskId, targetBranch) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch));
|
||||
});
|
||||
|
||||
_hub.On<string, string>("PlanningSubtaskMerged", (planningTaskId, subtaskId) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => PlanningSubtaskMergedEvent?.Invoke(planningTaskId, subtaskId));
|
||||
});
|
||||
|
||||
_hub.On<string, string, IReadOnlyList<string>>("PlanningMergeConflict", (planningTaskId, subtaskId, conflictedFiles) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, conflictedFiles));
|
||||
});
|
||||
|
||||
_hub.On<string>("PlanningMergeAborted", planningTaskId =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => PlanningMergeAbortedEvent?.Invoke(planningTaskId));
|
||||
});
|
||||
|
||||
_hub.On<string>("PlanningCompleted", planningTaskId =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => PlanningCompletedEvent?.Invoke(planningTaskId));
|
||||
});
|
||||
}
|
||||
|
||||
public Task StartAsync()
|
||||
@@ -362,6 +393,46 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<int>("GetPendingDraftCountAsync", taskId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _hub.InvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId);
|
||||
return result ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MergeAllPlanningAsync(string planningTaskId, string targetBranch)
|
||||
{
|
||||
await _hub.InvokeAsync("MergeAllPlanning", planningTaskId, targetBranch);
|
||||
}
|
||||
|
||||
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
||||
{
|
||||
await _hub.InvokeAsync("ContinuePlanningMerge", planningTaskId);
|
||||
}
|
||||
|
||||
public async Task AbortPlanningMergeAsync(string planningTaskId)
|
||||
{
|
||||
await _hub.InvokeAsync("AbortPlanningMerge", planningTaskId);
|
||||
}
|
||||
|
||||
// IWorkerClient explicit implementations (drop typed return values)
|
||||
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
||||
=> await StartPlanningSessionAsync(taskId, ct);
|
||||
|
||||
@@ -112,6 +112,15 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private string _newSubtaskTitle = "";
|
||||
|
||||
// Planning merge controls
|
||||
[ObservableProperty] private ObservableCollection<string> _mergeTargetBranches = new();
|
||||
[ObservableProperty] private string? _selectedMergeTarget;
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(MergeAllCommand))]
|
||||
private bool _canMergeAll;
|
||||
[ObservableProperty] private string? _mergeAllDisabledReason;
|
||||
[ObservableProperty] private string? _mergeAllError;
|
||||
|
||||
// Claude CLI stream-json parser + buffer for partial text deltas
|
||||
private readonly StreamLineFormatter _formatter = new();
|
||||
private readonly StringBuilder _claudeBuf = new();
|
||||
@@ -185,6 +194,18 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
_worker.WorktreeUpdatedEvent += taskId =>
|
||||
{
|
||||
if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId);
|
||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||
};
|
||||
|
||||
_worker.TaskUpdatedEvent += taskId =>
|
||||
{
|
||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||
};
|
||||
|
||||
Subtasks.CollectionChanged += (_, _) =>
|
||||
{
|
||||
RecomputeCanMergeAll();
|
||||
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -313,6 +334,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(TaskIdBadge));
|
||||
Log.Clear();
|
||||
Subtasks.Clear();
|
||||
MergeTargetBranches.Clear();
|
||||
SelectedMergeTarget = null;
|
||||
CanMergeAll = false;
|
||||
MergeAllDisabledReason = null;
|
||||
MergeAllError = null;
|
||||
_claudeBuf.Clear();
|
||||
|
||||
if (row == null)
|
||||
@@ -388,6 +414,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
ct.ThrowIfCancellationRequested();
|
||||
foreach (var s in subs)
|
||||
Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed });
|
||||
|
||||
if (entity.Status == ClaudeDo.Data.Models.TaskStatus.Planning ||
|
||||
entity.Status == ClaudeDo.Data.Models.TaskStatus.Planned)
|
||||
{
|
||||
await LoadPlanningChildrenAsync(row.Id, ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
@@ -445,6 +477,119 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
return path;
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task LoadPlanningChildrenAsync(string parentTaskId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var children = await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Worktree)
|
||||
.Where(t => t.ParentTaskId == parentTaskId)
|
||||
.ToListAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
var existing = Subtasks.FirstOrDefault(s => s.Id == child.Id);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Status = child.Status;
|
||||
existing.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
|
||||
}
|
||||
}
|
||||
|
||||
if (MergeTargetBranches.Count == 0)
|
||||
{
|
||||
var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
|
||||
if (childWithWorktree != null)
|
||||
{
|
||||
var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
|
||||
if (targets != null)
|
||||
{
|
||||
MergeTargetBranches.Clear();
|
||||
foreach (var b in targets.LocalBranches)
|
||||
MergeTargetBranches.Add(b);
|
||||
SelectedMergeTarget = targets.DefaultBranch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecomputeCanMergeAll();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task RefreshPlanningChildAsync(string childTaskId)
|
||||
{
|
||||
if (Task is null) return;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var child = await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Worktree)
|
||||
.FirstOrDefaultAsync(t => t.Id == childTaskId && t.ParentTaskId == Task.Id);
|
||||
if (child == null) return;
|
||||
|
||||
var existing = Subtasks.FirstOrDefault(s => s.Id == child.Id);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Status = child.Status;
|
||||
existing.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
|
||||
}
|
||||
|
||||
RecomputeCanMergeAll();
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
private void RecomputeCanMergeAll()
|
||||
{
|
||||
var notDone = Subtasks.Count(c => c.Status != ClaudeDo.Data.Models.TaskStatus.Done);
|
||||
if (notDone > 0)
|
||||
{
|
||||
CanMergeAll = false;
|
||||
MergeAllDisabledReason = $"{notDone} subtask(s) not done";
|
||||
return;
|
||||
}
|
||||
var badWt = Subtasks.FirstOrDefault(c =>
|
||||
c.WorktreeState == ClaudeDo.Data.Models.WorktreeState.Discarded ||
|
||||
c.WorktreeState == ClaudeDo.Data.Models.WorktreeState.Kept);
|
||||
if (badWt is not null)
|
||||
{
|
||||
CanMergeAll = false;
|
||||
MergeAllDisabledReason = "at least one worktree was discarded/kept";
|
||||
return;
|
||||
}
|
||||
CanMergeAll = true;
|
||||
MergeAllDisabledReason = null;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanReviewDiff))]
|
||||
private async System.Threading.Tasks.Task ReviewCombinedDiffAsync()
|
||||
{
|
||||
// TODO(Task 14): open PlanningDiffView once it exists
|
||||
await System.Threading.Tasks.Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool CanReviewDiff() => Task?.IsPlanningParent == true && Subtasks.Any();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanMergeAll))]
|
||||
private async System.Threading.Tasks.Task MergeAllAsync()
|
||||
{
|
||||
MergeAllError = null;
|
||||
try
|
||||
{
|
||||
await _worker.MergeAllPlanningAsync(Task!.Id, SelectedMergeTarget ?? "main");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MergeAllError = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
|
||||
{
|
||||
try
|
||||
@@ -665,4 +810,6 @@ public sealed partial class SubtaskRowViewModel : ViewModelBase
|
||||
public required string Id { get; init; }
|
||||
[ObservableProperty] private string _title = "";
|
||||
[ObservableProperty] private bool _done;
|
||||
[ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status;
|
||||
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active;
|
||||
}
|
||||
|
||||
@@ -138,6 +138,35 @@
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Planning merge section — visible only for planning parent tasks -->
|
||||
<Border Padding="18,12,18,12"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding Task.IsPlanningParent}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="section-label" Text="MERGE" Margin="0,0,0,2"/>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Merge target"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFaintBrush}"/>
|
||||
<ComboBox ItemsSource="{Binding MergeTargetBranches}"
|
||||
SelectedItem="{Binding SelectedMergeTarget, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Review combined diff"
|
||||
Command="{Binding ReviewCombinedDiffCommand}"/>
|
||||
<Button Content="Merge all subtasks"
|
||||
Command="{Binding MergeAllCommand}"
|
||||
ToolTip.Tip="{Binding MergeAllDisabledReason}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding MergeAllError}"
|
||||
Foreground="OrangeRed"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding MergeAllError, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Steps section -->
|
||||
<Border Padding="18,12,18,12"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
|
||||
134
tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs
Normal file
134
tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user