Opens a modal when PlanningMergeConflict fires, listing conflicted files with options to open in VS Code, continue, or abort the merge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
140 lines
6.2 KiB
C#
140 lines
6.2 KiB
C#
using System.ComponentModel;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Ui.Services;
|
|
using ClaudeDo.Ui.ViewModels.Planning;
|
|
|
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
|
|
|
public class ConflictResolutionViewModelTests
|
|
{
|
|
// ------------------------------------------------------------------ fake
|
|
private sealed class FakeWorker : IWorkerClient
|
|
{
|
|
public bool IsConnected => false;
|
|
|
|
public string? ContinueCalledWith { get; private set; }
|
|
public string? AbortCalledWith { get; private set; }
|
|
public Exception? ContinueThrows { get; set; }
|
|
public Exception? AbortThrows { get; set; }
|
|
|
|
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 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<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)
|
|
{
|
|
ContinueCalledWith = planningTaskId;
|
|
if (ContinueThrows is not null) throw ContinueThrows;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task AbortPlanningMergeAsync(string planningTaskId)
|
|
{
|
|
AbortCalledWith = planningTaskId;
|
|
if (AbortThrows is not null) throw AbortThrows;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
private static ConflictResolutionViewModel BuildVm(FakeWorker worker, string planningTaskId = "plan-1") =>
|
|
new ConflictResolutionViewModel(
|
|
worker,
|
|
planningTaskId,
|
|
subtaskTitle: "My subtask",
|
|
targetBranch: "main",
|
|
conflictedFiles: new[] { "src/Foo.cs", "src/Bar.cs" },
|
|
worktreePath: "C:/worktrees/plan-1");
|
|
|
|
// ------------------------------------------------------------------ tests
|
|
|
|
[Fact]
|
|
public async Task ContinueAsync_CallsHub_AndClosesOnSuccess()
|
|
{
|
|
var worker = new FakeWorker();
|
|
var vm = BuildVm(worker, "plan-42");
|
|
bool closeCalled = false;
|
|
vm.CloseRequested = () => closeCalled = true;
|
|
|
|
await vm.ContinueCommand.ExecuteAsync(null);
|
|
|
|
Assert.Equal("plan-42", worker.ContinueCalledWith);
|
|
Assert.True(closeCalled);
|
|
Assert.Null(vm.ActionError);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ContinueAsync_HubThrows_ShowsActionErrorAndStaysOpen()
|
|
{
|
|
var worker = new FakeWorker { ContinueThrows = new InvalidOperationException("hub down") };
|
|
var vm = BuildVm(worker);
|
|
bool closeCalled = false;
|
|
vm.CloseRequested = () => closeCalled = true;
|
|
|
|
await vm.ContinueCommand.ExecuteAsync(null);
|
|
|
|
Assert.False(closeCalled);
|
|
Assert.Equal("hub down", vm.ActionError);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AbortAsync_CallsHub_AndClosesOnSuccess()
|
|
{
|
|
var worker = new FakeWorker();
|
|
var vm = BuildVm(worker, "plan-99");
|
|
bool closeCalled = false;
|
|
vm.CloseRequested = () => closeCalled = true;
|
|
|
|
await vm.AbortCommand.ExecuteAsync(null);
|
|
|
|
Assert.Equal("plan-99", worker.AbortCalledWith);
|
|
Assert.True(closeCalled);
|
|
Assert.Null(vm.ActionError);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AbortAsync_HubThrows_ShowsActionError()
|
|
{
|
|
var worker = new FakeWorker { AbortThrows = new InvalidOperationException("abort failed") };
|
|
var vm = BuildVm(worker);
|
|
bool closeCalled = false;
|
|
vm.CloseRequested = () => closeCalled = true;
|
|
|
|
await vm.AbortCommand.ExecuteAsync(null);
|
|
|
|
Assert.False(closeCalled);
|
|
Assert.Equal("abort failed", vm.ActionError);
|
|
}
|
|
|
|
// OpenInVsCode is not unit-tested here because abstracting Process.Start
|
|
// would require an indirection layer that isn't part of the approved design.
|
|
// The error path is covered by the VsCodeError property being set on catch.
|
|
}
|