feat(ui): add aggregated diff viewer for planning tasks
Implements Task 14: PlanningDiffView (Window), PlanningDiffViewModel, ShowPlanningDiffModal callback wired in DetailsIslandView, and 5 xUnit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
163
tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs
Normal file
163
tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
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<string, string, DateTime>? TaskStartedEvent;
|
||||
public event Action<string, string, string, DateTime>? TaskFinishedEvent;
|
||||
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 bool IsConnected => false;
|
||||
|
||||
public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>();
|
||||
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<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(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<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
|
||||
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) => Task.FromResult<MergeTargetsDto?>(null);
|
||||
public Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId) =>
|
||||
Task.FromResult(AggregateResult);
|
||||
public Task<CombinedDiffResultDto?> 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!);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user