Files
ClaudeDo/tests/ClaudeDo.Data.Tests/ConflictMarkerParserTests.cs
Mika Kuns e779e13654 feat(merge): real conflict-hunk parsing pipeline (chunk 2 backend)
Replace the whole-file conflict model with line-level hunks, the
foundation for the full in-app merge editor.

- ConflictMarkerParser: parses git conflict markers (incl. diff3 base)
  into ordered stable/conflict MergeSegments; exact round-trip + Compose
- GitService.MergeNoFfAsync passes -c merge.conflictStyle=diff3 so the
  working tree carries the merge base in conflict markers
- TaskMergeService.GetConflictDocumentsAsync: reads each conflicted file,
  parses into segments, flags binary files
- hub GetMergeConflictDocuments + DTOs (MergeConflictDocumentsDto/
  ConflictDocumentDto/MergeSegmentDto), IWorkerClient + both fakes
- tests: 8 parser unit tests + a real-git integration test asserting
  line-level hunks with a diff3 base
2026-06-18 16:22:56 +02:00

124 lines
4.0 KiB
C#

using System.Linq;
using ClaudeDo.Data.Git;
using Xunit;
namespace ClaudeDo.Data.Tests;
public class ConflictMarkerParserTests
{
[Fact]
public void NoMarkers_YieldsSingleStableSegment_AndRoundTrips()
{
const string text = "just\nsome\nplain\nlines\n";
var segments = ConflictMarkerParser.Parse(text);
var only = Assert.Single(segments);
Assert.False(only.IsConflict);
Assert.Equal(text, ConflictMarkerParser.Compose(segments, c => c.Ours));
Assert.False(ConflictMarkerParser.HasConflicts(text));
}
[Fact]
public void SimpleConflict_SplitsIntoStable_Conflict_Stable()
{
const string text =
"line1\n<<<<<<< HEAD\nours line\n=======\ntheirs line\n>>>>>>> branch\nline2\n";
var segments = ConflictMarkerParser.Parse(text);
Assert.Equal(3, segments.Count);
Assert.Equal("line1\n", segments[0].Text);
Assert.True(segments[1].IsConflict);
Assert.Equal("ours line\n", segments[1].Ours);
Assert.Equal("theirs line\n", segments[1].Theirs);
Assert.Null(segments[1].Base);
Assert.Equal("line2\n", segments[2].Text);
Assert.True(ConflictMarkerParser.HasConflicts(text));
}
[Fact]
public void AcceptingOurs_Or_Theirs_ProducesTheResolvedFile()
{
const string text =
"line1\n<<<<<<< HEAD\nours line\n=======\ntheirs line\n>>>>>>> branch\nline2\n";
var segments = ConflictMarkerParser.Parse(text);
Assert.Equal("line1\nours line\nline2\n",
ConflictMarkerParser.Compose(segments, c => c.Ours));
Assert.Equal("line1\ntheirs line\nline2\n",
ConflictMarkerParser.Compose(segments, c => c.Theirs));
// "Accept both" = ours followed by theirs.
Assert.Equal("line1\nours line\ntheirs line\nline2\n",
ConflictMarkerParser.Compose(segments, c => c.Ours + c.Theirs));
}
[Fact]
public void Diff3Style_CapturesTheMergeBase()
{
const string text =
"a\n<<<<<<< HEAD\nX\n||||||| base\nB\n=======\nY\n>>>>>>> branch\nz\n";
var segments = ConflictMarkerParser.Parse(text);
Assert.Equal(3, segments.Count);
Assert.Equal("X\n", segments[1].Ours);
Assert.Equal("B\n", segments[1].Base);
Assert.Equal("Y\n", segments[1].Theirs);
}
[Fact]
public void MultipleConflicts_AreEachCaptured()
{
const string text =
"<<<<<<< HEAD\nA\n=======\nB\n>>>>>>> br\nmid\n<<<<<<< HEAD\nC\n=======\nD\n>>>>>>> br\n";
var segments = ConflictMarkerParser.Parse(text);
Assert.Equal(3, segments.Count);
Assert.True(segments[0].IsConflict);
Assert.Equal("A\n", segments[0].Ours);
Assert.Equal("mid\n", segments[1].Text);
Assert.True(segments[2].IsConflict);
Assert.Equal("D\n", segments[2].Theirs);
}
[Fact]
public void CrlfLineEndings_ArePreserved()
{
const string text =
"a\r\n<<<<<<< HEAD\r\nX\r\n=======\r\nY\r\n>>>>>>> br\r\nb\r\n";
var segments = ConflictMarkerParser.Parse(text);
Assert.Equal("a\r\nX\r\nb\r\n", ConflictMarkerParser.Compose(segments, c => c.Ours));
}
[Fact]
public void ConflictAtEndOfFile_WithoutTrailingNewline_IsParsed()
{
const string text = "a\n<<<<<<< HEAD\nX\n=======\nY\n>>>>>>> br";
var segments = ConflictMarkerParser.Parse(text);
Assert.Equal(2, segments.Count);
Assert.Equal("a\n", segments[0].Text);
Assert.True(segments[1].IsConflict);
Assert.Equal("X\n", segments[1].Ours);
Assert.Equal("Y\n", segments[1].Theirs);
}
[Fact]
public void SevenEqualsInOrdinaryText_IsNotTreatedAsAConflict()
{
const string text = "title\n=======\nbody\n";
var segments = ConflictMarkerParser.Parse(text);
var only = Assert.Single(segments);
Assert.False(only.IsConflict);
Assert.Equal(text, ConflictMarkerParser.Compose(segments, c => c.Ours));
}
}