using System.IO; using ClaudeDo.Localization; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals; namespace ClaudeDo.Ui.Tests.ViewModels; public class DiffViewerViewModelTests { public DiffViewerViewModelTests() { var dir = AppContext.BaseDirectory; while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales"))) dir = Path.GetDirectoryName(dir); Loc.Current = new Localizer( LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en"); } private sealed class FakePlanningWorker : StubWorkerClient { public IReadOnlyList AggregateResult { get; set; } = Array.Empty(); public CombinedDiffResultDto? CombinedResult { get; set; } public override Task> GetPlanningAggregateAsync(string planningTaskId) => Task.FromResult(AggregateResult); public override Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult(CombinedResult); } // ── Files mode: commit-range guards (ported from DiffModal) ── [Fact] public async Task CommitRange_NullHeadCommit_ShowsUnavailable() { var vm = new DiffViewerViewModel(null!, new FakePlanningWorker()); vm.ConfigureCommitRange("/some/repo", "abc123", null); await vm.LoadAsync(); Assert.Empty(vm.FileTree); Assert.NotNull(vm.StatusMessage); Assert.Contains("no longer available", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task CommitRange_NullBaseRef_ShowsUnavailable() { var vm = new DiffViewerViewModel(null!, new FakePlanningWorker()); vm.ConfigureCommitRange("/some/repo", null, "def456"); await vm.LoadAsync(); Assert.Empty(vm.FileTree); Assert.NotNull(vm.StatusMessage); Assert.Contains("no longer available", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); } // ── File tree building ── [Fact] public void DiffTree_Build_GroupsByFolder_AndCarriesFileLeaves() { var files = new List { new() { Path = "src/a/x.cs", Additions = 3, Deletions = 1 }, new() { Path = "src/a/y.cs" }, new() { Path = "readme.md", Status = DiffFileStatus.Added }, }; var roots = DiffTree.Build(files); Assert.Equal(2, roots.Count); // "src" dir + "readme.md" leaf var src = roots.First(n => n is { IsDirectory: true, Name: "src" }); var a = Assert.Single(src.Children); Assert.True(a is { IsDirectory: true, Name: "a" }); Assert.Equal(2, a.Children.Count); var x = a.Children.First(n => n.Name == "x.cs"); Assert.False(x.IsDirectory); Assert.NotNull(x.File); Assert.Equal(3, x.Additions); var readme = roots.First(n => n is { IsDirectory: false, Name: "readme.md" }); Assert.Equal("A", readme.StatusCode); // First leaf walks depth-first in insertion order → src/a/x.cs. Assert.Equal(x, DiffTree.FirstLeaf(roots)); } // ── Planning mode (ported from PlanningDiff) ── [Fact] public async Task Planning_Load_PopulatesSubtasks_SelectsFirst() { 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 DiffViewerViewModel(null!, fake); vm.ConfigurePlanning("plan-1", "main"); await vm.LoadAsync(); Assert.Equal(2, vm.Subtasks.Count); Assert.Equal(vm.Subtasks[0], vm.SelectedSubtask); } [Fact] public async Task Planning_SelectSubtask_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 DiffViewerViewModel(null!, fake); vm.ConfigurePlanning("plan-1", "main"); await vm.LoadAsync(); vm.SelectedSubtask = vm.Subtasks[1]; Assert.Equal("DIFF-B", vm.DisplayedDiff); } [Fact] public async Task Planning_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 DiffViewerViewModel(null!, fake); vm.ConfigurePlanning("plan-1", "main"); await vm.LoadAsync(); vm.IsCombinedMode = true; 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 Planning_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 DiffViewerViewModel(null!, fake); vm.ConfigurePlanning("plan-1", "main"); await vm.LoadAsync(); 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 Planning_ToggleCombined_HubReturnsNull_ShowsError() { var fake = new FakePlanningWorker { AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") }, CombinedResult = null, }; var vm = new DiffViewerViewModel(null!, fake); vm.ConfigurePlanning("plan-1", "main"); await vm.LoadAsync(); 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!); } }