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? TaskStartedEvent; public event Action? TaskFinishedEvent; 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 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> GetAgentsAsync() => Task.FromResult(new List()); public Task GetListConfigAsync(string listId) => Task.FromResult(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 GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0); public Task GetMergeTargetsAsync(string taskId) => Task.FromResult(null); 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) { 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. }