using System.Collections.Generic; using System.Threading.Tasks; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Conflicts; using Xunit; namespace ClaudeDo.Ui.Tests.ViewModels; public class ConflictResolverViewModelTests { // A file split as: stable "a\n", one conflict, stable "z\n". private static ConflictDocumentDto OneConflictFile(string path = "README.md") => new(path, false, new[] { new MergeSegmentDto(false, "a\n", "", null, ""), new MergeSegmentDto(true, "", "ours\n", "base\n", "theirs\n"), new MergeSegmentDto(false, "z\n", "", null, ""), }); private sealed class FakeWorker : StubWorkerClient { public string? WrittenPath; public string? WrittenContent; public bool Continued; public bool Aborted; public string ContinueStatus = "merged"; public IReadOnlyList Docs = new[] { OneConflictFile() }; public override Task StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", new[] { "README.md" }, null)); public override Task GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, Docs)); public override Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) { WrittenPath = path; WrittenContent = resolvedContent; return Task.CompletedTask; } public override Task ContinueConflictMergeAsync(string taskId) { Continued = true; return Task.FromResult(new MergeResultDto(ContinueStatus, System.Array.Empty(), null)); } public override Task AbortConflictMergeAsync(string taskId) { Aborted = true; return Task.CompletedTask; } } [Fact] public async Task OpenAsync_LoadsConflicts_AndBlocksContinueUntilResolved() { var vm = new ConflictResolverViewModel(new FakeWorker(), "task-1"); var hasConflicts = await vm.OpenAsync("main"); Assert.True(hasConflicts); var file = Assert.Single(vm.Files); Assert.Equal("README.md", file.Path); Assert.Equal(1, vm.TotalConflicts); Assert.NotNull(vm.Current); Assert.False(vm.CanContinue); vm.Current!.AcceptTheirsCommand.Execute(null); Assert.True(vm.CanContinue); Assert.Equal(1, vm.ResolvedCount); } [Fact] public async Task Continue_WritesComposedFile_AndClosesOnMerged() { var worker = new FakeWorker(); var vm = new ConflictResolverViewModel(worker, "task-1"); var closed = false; vm.CloseRequested = () => closed = true; await vm.OpenAsync("main"); vm.Current!.AcceptOursCommand.Execute(null); // choose "ours\n" for the single conflict await vm.ContinueCommand.ExecuteAsync(null); Assert.Equal("README.md", worker.WrittenPath); // stable "a\n" + chosen "ours\n" + stable "z\n" Assert.Equal("a\nours\nz\n", worker.WrittenContent); Assert.True(worker.Continued); Assert.True(closed); } [Fact] public async Task Continue_StaysOpenAndReportsError_WhenStillConflicted() { var worker = new FakeWorker { ContinueStatus = "conflict" }; var vm = new ConflictResolverViewModel(worker, "task-1"); var closed = false; vm.CloseRequested = () => closed = true; await vm.OpenAsync("main"); vm.Current!.AcceptBothCommand.Execute(null); await vm.ContinueCommand.ExecuteAsync(null); Assert.False(closed); Assert.NotNull(vm.Error); } [Fact] public async Task Navigation_MovesBetweenConflicts_AcrossTheFlattenedList() { var worker = new FakeWorker { Docs = new[] { new ConflictDocumentDto("a.cs", false, new[] { new MergeSegmentDto(true, "", "o1\n", null, "t1\n"), new MergeSegmentDto(false, "mid\n", "", null, ""), new MergeSegmentDto(true, "", "o2\n", null, "t2\n"), }), }, }; var vm = new ConflictResolverViewModel(worker, "task-1"); await vm.OpenAsync("main"); Assert.Equal(2, vm.TotalConflicts); Assert.Equal("o1\n", vm.Current!.Ours); Assert.False(vm.PreviousCommand.CanExecute(null)); Assert.True(vm.NextCommand.CanExecute(null)); vm.NextCommand.Execute(null); Assert.Equal("o2\n", vm.Current!.Ours); Assert.False(vm.NextCommand.CanExecute(null)); Assert.True(vm.PreviousCommand.CanExecute(null)); } [Fact] public async Task BinaryConflict_BlocksContinue() { var worker = new FakeWorker { Docs = new[] { new ConflictDocumentDto("logo.png", true, System.Array.Empty()) }, }; var vm = new ConflictResolverViewModel(worker, "task-1"); var hasConflicts = await vm.OpenAsync("main"); Assert.True(hasConflicts); // file is shown Assert.True(vm.HasBinaryFiles); Assert.Equal(0, vm.TotalConflicts); // nothing to resolve in-app Assert.False(vm.CanContinue); // binary blocks continue } [Fact] public async Task Abort_CallsWorkerAndCloses() { var worker = new FakeWorker(); var vm = new ConflictResolverViewModel(worker, "task-1"); var closed = false; vm.CloseRequested = () => closed = true; await vm.AbortCommand.ExecuteAsync(null); Assert.True(worker.Aborted); Assert.True(closed); } }