using ClaudeDo.Ui.ViewModels.Modals; using Xunit; namespace ClaudeDo.Ui.Tests.ViewModels; public class DiffAlignmentTests { private static DiffLineViewModel Ctx(int oldNo, int newNo, string text) => new() { Kind = DiffLineKind.Ctx, OldNo = oldNo, NewNo = newNo, Text = text }; private static DiffLineViewModel Del(int oldNo, string text) => new() { Kind = DiffLineKind.Del, OldNo = oldNo, Text = text }; private static DiffLineViewModel Add(int newNo, string text) => new() { Kind = DiffLineKind.Add, NewNo = newNo, Text = text }; [Fact] public void NullOrEmpty_YieldsEmptyDiff() { Assert.Empty(DiffAlignment.Build(null).SplitRows); Assert.Empty(DiffAlignment.Build(Array.Empty()).SplitRows); } [Fact] public void ContextOnly_MirrorsBothSides_WithNoFillers() { var result = DiffAlignment.Build(new[] { Ctx(1, 1, "a"), Ctx(2, 2, "b") }); Assert.Equal(2, result.SplitRows.Count); Assert.All(result.SplitRows, r => { Assert.Equal(AlignedSide.Ctx, r.LeftKind); Assert.Equal(AlignedSide.Ctx, r.RightKind); Assert.Equal(r.LeftText, r.RightText); }); Assert.Equal("a\nb", result.LeftText); Assert.Equal("a\nb", result.RightText); } [Fact] public void EqualSizedChangeBlock_PairsOneToOne() { var result = DiffAlignment.Build(new[] { Del(1, "old one"), Del(2, "old two"), Add(1, "new one"), Add(2, "new two"), }); Assert.Equal(2, result.SplitRows.Count); Assert.Equal(AlignedSide.Del, result.SplitRows[0].LeftKind); Assert.Equal(AlignedSide.Add, result.SplitRows[0].RightKind); Assert.Equal("old one", result.SplitRows[0].LeftText); Assert.Equal("new one", result.SplitRows[0].RightText); Assert.Equal(1, result.SplitRows[0].OldNo!.Value); Assert.Equal(1, result.SplitRows[0].NewNo!.Value); } [Fact] public void MoreAddsThanDels_PadsTheLeftSideWithFillers() { var result = DiffAlignment.Build(new[] { Del(1, "d1"), Add(1, "a1"), Add(2, "a2"), Add(3, "a3"), }); Assert.Equal(3, result.SplitRows.Count); Assert.Equal(AlignedSide.Del, result.SplitRows[0].LeftKind); Assert.Equal(AlignedSide.Filler, result.SplitRows[1].LeftKind); Assert.Equal(AlignedSide.Filler, result.SplitRows[2].LeftKind); Assert.Equal("", result.SplitRows[1].LeftText); Assert.Null(result.SplitRows[1].OldNo); Assert.All(result.SplitRows, r => Assert.Equal(AlignedSide.Add, r.RightKind)); } [Fact] public void MoreDelsThanAdds_PadsTheRightSideWithFillers() { var result = DiffAlignment.Build(new[] { Del(1, "d1"), Del(2, "d2"), Del(3, "d3"), Add(1, "a1"), }); Assert.Equal(3, result.SplitRows.Count); Assert.Equal(AlignedSide.Add, result.SplitRows[0].RightKind); Assert.Equal(AlignedSide.Filler, result.SplitRows[1].RightKind); Assert.Equal(AlignedSide.Filler, result.SplitRows[2].RightKind); Assert.All(result.SplitRows, r => Assert.Equal(AlignedSide.Del, r.LeftKind)); } [Fact] public void NonContiguousLineNumbers_InsertOneGapRow() { var result = DiffAlignment.Build(new[] { Ctx(1, 1, "a"), Ctx(40, 40, "b") }); Assert.Equal(3, result.SplitRows.Count); Assert.Equal(AlignedSide.Gap, result.SplitRows[1].LeftKind); Assert.Equal(AlignedSide.Gap, result.SplitRows[1].RightKind); Assert.Null(result.SplitRows[1].OldNo); Assert.Null(result.SplitRows[1].NewNo); } [Fact] public void FirstLineNeverProducesALeadingGap() { var result = DiffAlignment.Build(new[] { Ctx(120, 118, "a") }); Assert.Single(result.SplitRows); Assert.Equal(AlignedSide.Ctx, result.SplitRows[0].LeftKind); } [Fact] public void UnifiedRows_KeepGitOrder_DeletionsThenAdditions() { var result = DiffAlignment.Build(new[] { Ctx(1, 1, "keep"), Del(2, "d1"), Del(3, "d2"), Add(2, "a1"), }); Assert.Equal(4, result.UnifiedRows.Count); Assert.Equal(AlignedSide.Ctx, result.UnifiedRows[0].Kind); Assert.Equal(AlignedSide.Del, result.UnifiedRows[1].Kind); Assert.Equal(AlignedSide.Del, result.UnifiedRows[2].Kind); Assert.Equal(AlignedSide.Add, result.UnifiedRows[3].Kind); Assert.Equal("keep\nd1\nd2\na1", result.UnifiedText); } [Fact] public void RowIndex_MapsToDocumentLine_OnBothSides() { var result = DiffAlignment.Build(new[] { Ctx(1, 1, "a"), Del(2, "d"), Add(2, "x"), Add(3, "y"), Ctx(3, 4, "b"), }); var leftLines = result.LeftText.Split('\n'); var rightLines = result.RightText.Split('\n'); Assert.Equal(result.SplitRows.Count, leftLines.Length); Assert.Equal(result.SplitRows.Count, rightLines.Length); for (var i = 0; i < result.SplitRows.Count; i++) { Assert.Equal(result.SplitRows[i].LeftText, leftLines[i]); Assert.Equal(result.SplitRows[i].RightText, rightLines[i]); } var unifiedLines = result.UnifiedText.Split('\n'); Assert.Equal(result.UnifiedRows.Count, unifiedLines.Length); } [Fact] public void FileHeaderRows_AreIgnored() { var result = DiffAlignment.Build(new[] { new DiffLineViewModel { Kind = DiffLineKind.File, Text = "src/Foo.cs" }, Ctx(1, 1, "a"), }); Assert.Single(result.SplitRows); Assert.Equal("a", result.SplitRows[0].LeftText); } [Fact] public void WordDiff_HighlightsOnlyTheChangedToken() { var result = DiffAlignment.Build(new[] { Del(1, " var x = 1;"), Add(1, " var x = 2;"), }); var row = Assert.Single(result.SplitRows); var left = Assert.Single(row.LeftSpans); var right = Assert.Single(row.RightSpans); Assert.Equal(new TextSpan(12, 1), left); Assert.Equal(new TextSpan(12, 1), right); Assert.Equal("1", row.LeftText.Substring(left.Start, left.Length)); Assert.Equal("2", row.RightText.Substring(right.Start, right.Length)); } [Fact] public void WordDiff_MergesAdjacentChangedTokensIntoOneSpan() { var result = DiffAlignment.Build(new[] { Del(1, "call(a, b);"), Add(1, "call(zz, b);"), }); var row = Assert.Single(result.SplitRows); var right = Assert.Single(row.RightSpans); Assert.Equal("zz", row.RightText.Substring(right.Start, right.Length)); } [Fact] public void WordDiff_SkippedWhenTheLinesAreUnrelated() { var result = DiffAlignment.Build(new[] { Del(1, "public void Alpha()"), Add(1, "return 42;"), }); var row = Assert.Single(result.SplitRows); Assert.Empty(row.LeftSpans); Assert.Empty(row.RightSpans); } [Fact] public void WordDiff_SkippedOnVeryLongLines() { var left = new string('a', DiffAlignment.MaxWordDiffChars + 1); var result = DiffAlignment.Build(new[] { Del(1, left), Add(1, left + "b") }); var row = Assert.Single(result.SplitRows); Assert.Empty(row.LeftSpans); Assert.Empty(row.RightSpans); } [Fact] public void WordDiff_SkippedWhenTokenCountExceedsTheCap() { var many = string.Join(" ", Enumerable.Range(0, DiffAlignment.MaxWordDiffTokens + 10).Select(n => $"t{n}")); var result = DiffAlignment.Build(new[] { Del(1, many), Add(1, many + " x") }); var row = Assert.Single(result.SplitRows); Assert.Empty(row.LeftSpans); Assert.Empty(row.RightSpans); } [Fact] public void WordDiff_IdenticalTextYieldsNoSpans() { var result = DiffAlignment.Build(new[] { Del(1, "same"), Add(1, "same") }); var row = Assert.Single(result.SplitRows); Assert.Empty(row.LeftSpans); Assert.Empty(row.RightSpans); } [Fact] public void WordDiff_UnpairedOverhangRowsHaveNoSpans() { var result = DiffAlignment.Build(new[] { Del(1, "var x = 1;"), Add(1, "var x = 2;"), Add(2, "var y = 3;"), }); Assert.NotEmpty(result.SplitRows[0].RightSpans); Assert.Empty(result.SplitRows[1].RightSpans); } }