package main import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // --- normalizeForVerify --- func TestNormalizeForVerify_RemovesEmptyBrParagraph(t *testing.T) { tests := []struct { name string input string want string }{ { name: "br with space before slash", input: `


text

`, want: `

text

`, }, { name: "br without space", input: `


text

`, want: `

text

`, }, { name: "br with whitespace around", input: "

\n
\n

text

", want: "

text

", }, { name: "no empty paragraphs", input: `

hello

`, want: `

hello

`, }, { name: "multiple empty paragraphs", input: `



text

`, want: `

text

`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, normalizeForVerify(tt.input)) }) } } func TestNormalizeForVerify_SpanInsideCode(t *testing.T) { tests := []struct { name string input string want string }{ { name: "span inside code unwrapped", input: `hello world`, want: `hello world`, }, { name: "span with attributes inside code", input: `a : `, want: `a : `, }, { name: "no span inside code", input: `plain`, want: `plain`, }, { name: "span outside code untouched", input: `

text

`, want: `

text

`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, normalizeForVerify(tt.input)) }) } } func TestNormalizeForVerify_AdjacentCodeMerged(t *testing.T) { tests := []struct { name string input string want string }{ { name: "directly adjacent", input: `helloworld`, want: `helloworld`, }, { name: "with whitespace between", input: `hello world`, want: `hello world`, }, { name: "single code element untouched", input: `hello`, want: `hello`, }, { name: "combined: span inside + adjacent merge", input: `plan : vclock`, want: `plan : vclock`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, normalizeForVerify(tt.input)) }) } } // --- computeDiffOps --- func TestComputeDiffOps_IdenticalInputs(t *testing.T) { lines := []string{"a", "b", "c"} ops := computeDiffOps(lines, lines) require.Len(t, ops, 3) for _, op := range ops { assert.Equal(t, opEqual, op.op) } } func TestComputeDiffOps_CompletelyDifferent(t *testing.T) { a := []string{"a", "b"} b := []string{"x", "y"} ops := computeDiffOps(a, b) var removes, adds int for _, op := range ops { switch op.op { case opRemove: removes++ case opAdd: adds++ } } assert.Equal(t, 2, removes) assert.Equal(t, 2, adds) } func TestComputeDiffOps_EmptyInputs(t *testing.T) { assert.Empty(t, computeDiffOps(nil, nil)) assert.Empty(t, computeDiffOps([]string{}, []string{})) } func TestComputeDiffOps_OneEmpty(t *testing.T) { ops := computeDiffOps([]string{"a", "b"}, nil) require.Len(t, ops, 2) for _, op := range ops { assert.Equal(t, opRemove, op.op) } ops = computeDiffOps(nil, []string{"x", "y"}) require.Len(t, ops, 2) for _, op := range ops { assert.Equal(t, opAdd, op.op) } } func TestComputeDiffOps_SingleLineChange(t *testing.T) { a := []string{"aaa", "bbb", "ccc"} b := []string{"aaa", "BBB", "ccc"} ops := computeDiffOps(a, b) // Should be: equal(aaa), remove(bbb), add(BBB), equal(ccc) require.Len(t, ops, 4) assert.Equal(t, opEqual, ops[0].op) assert.Equal(t, "aaa", ops[0].text) assert.Equal(t, opRemove, ops[1].op) assert.Equal(t, "bbb", ops[1].text) assert.Equal(t, opAdd, ops[2].op) assert.Equal(t, "BBB", ops[2].text) assert.Equal(t, opEqual, ops[3].op) assert.Equal(t, "ccc", ops[3].text) } func TestComputeDiffOps_LineNumbers(t *testing.T) { a := []string{"same", "old"} b := []string{"same", "new"} ops := computeDiffOps(a, b) // equal: lineA=1, lineB=1 assert.Equal(t, 1, ops[0].lineA) assert.Equal(t, 1, ops[0].lineB) // remove: lineA=2, lineB=-1 assert.Equal(t, 2, ops[1].lineA) assert.Equal(t, -1, ops[1].lineB) // add: lineA=-1, lineB=2 assert.Equal(t, -1, ops[2].lineA) assert.Equal(t, 2, ops[2].lineB) } func TestComputeDiffOps_Insertion(t *testing.T) { a := []string{"a", "c"} b := []string{"a", "b", "c"} ops := computeDiffOps(a, b) require.Len(t, ops, 3) assert.Equal(t, opEqual, ops[0].op) assert.Equal(t, opAdd, ops[1].op) assert.Equal(t, "b", ops[1].text) assert.Equal(t, opEqual, ops[2].op) } func TestComputeDiffOps_Deletion(t *testing.T) { a := []string{"a", "b", "c"} b := []string{"a", "c"} ops := computeDiffOps(a, b) require.Len(t, ops, 3) assert.Equal(t, opEqual, ops[0].op) assert.Equal(t, opRemove, ops[1].op) assert.Equal(t, "b", ops[1].text) assert.Equal(t, opEqual, ops[2].op) } // --- buildHunks --- func TestBuildHunks_NoChanges(t *testing.T) { ops := computeDiffOps([]string{"a", "b", "c"}, []string{"a", "b", "c"}) hunks := buildHunks(ops, 3) assert.Empty(t, hunks) } func TestBuildHunks_SingleChange(t *testing.T) { a := []string{"1", "2", "3", "4", "5"} b := []string{"1", "2", "X", "4", "5"} ops := computeDiffOps(a, b) hunks := buildHunks(ops, 1) require.Len(t, hunks, 1) h := hunks[0] // Context=1: line 2 (before) + remove(3)/add(X) + line 4 (after) = 3 each side assert.Equal(t, 3, h.countA) // 2, remove(3), 4 assert.Equal(t, 3, h.countB) // 2, add(X), 4 } func TestBuildHunks_TwoSeparateChanges(t *testing.T) { // Changes far enough apart to be separate hunks a := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"} b := []string{"1", "X", "3", "4", "5", "6", "7", "8", "9", "10", "Y", "12"} ops := computeDiffOps(a, b) hunks := buildHunks(ops, 1) assert.Len(t, hunks, 2) } func TestBuildHunks_MergesNearbyChanges(t *testing.T) { // Two changes only 2 lines apart with ctx=3 should merge a := []string{"1", "2", "3", "4", "5", "6", "7"} b := []string{"1", "X", "3", "4", "Y", "6", "7"} ops := computeDiffOps(a, b) hunks := buildHunks(ops, 3) assert.Len(t, hunks, 1, "nearby changes should merge into one hunk") } func TestBuildHunks_ContextClampedToFileEdge(t *testing.T) { // Change at line 1 — context shouldn't go negative a := []string{"old", "same"} b := []string{"new", "same"} ops := computeDiffOps(a, b) hunks := buildHunks(ops, 3) require.Len(t, hunks, 1) assert.Equal(t, 1, hunks[0].startA) assert.Equal(t, 1, hunks[0].startB) } func TestBuildHunks_Counts(t *testing.T) { a := []string{"ctx", "old1", "old2", "ctx"} b := []string{"ctx", "new1", "ctx"} ops := computeDiffOps(a, b) hunks := buildHunks(ops, 1) require.Len(t, hunks, 1) h := hunks[0] // countA = context lines + removed lines // countB = context lines + added lines aLines := 0 bLines := 0 for _, dl := range h.lines { if dl.op == opEqual || dl.op == opRemove { aLines++ } if dl.op == opEqual || dl.op == opAdd { bLines++ } } assert.Equal(t, aLines, h.countA) assert.Equal(t, bLines, h.countB) } // --- inlineHighlight --- func TestInlineHighlight_IdenticalLines(t *testing.T) { a, b := inlineHighlight("same text", "same text") // No ANSI escapes added when lines are identical assert.Equal(t, "same text", a) assert.Equal(t, "same text", b) } func TestInlineHighlight_SingleWordDiff(t *testing.T) { a, b := inlineHighlight("hello world", "hello Earth") // "hello " is common prefix, no common suffix assert.Contains(t, a, "hello ") assert.Contains(t, b, "hello ") // Changed part should have bold marker assert.Contains(t, a, ansiBold) assert.Contains(t, b, ansiBold) // Changed part should have appropriate background assert.Contains(t, a, ansiRedBg) assert.Contains(t, b, ansiGrnBg) } func TestInlineHighlight_MiddleChange(t *testing.T) { a, b := inlineHighlight("abc-OLD-xyz", "abc-NEW-xyz") // Common prefix "abc-", common suffix "-xyz" // Both lines should highlight "OLD" / "NEW" in bold assert.Contains(t, a, ansiBold) assert.Contains(t, b, ansiBold) assert.Contains(t, a, "OLD") assert.Contains(t, b, "NEW") // Prefix and suffix present without bold assertPlainContains(t, a, "abc-") assertPlainContains(t, b, "abc-") } func TestInlineHighlight_PrefixOnlyDifference(t *testing.T) { a, b := inlineHighlight("XXX-same", "YYY-same") // "-same" is common suffix assert.Contains(t, a, "XXX") assert.Contains(t, b, "YYY") assert.Contains(t, a, ansiBold) } func TestInlineHighlight_SuffixOnlyDifference(t *testing.T) { a, b := inlineHighlight("same-XXX", "same-YYY") // "same-" is common prefix assert.Contains(t, a, "XXX") assert.Contains(t, b, "YYY") assert.Contains(t, a, ansiBold) } func TestInlineHighlight_EmptyVsNonEmpty(t *testing.T) { _, b := inlineHighlight("", "added") assert.Contains(t, b, "added") assert.Contains(t, b, ansiBold) } func TestInlineHighlight_Unicode(t *testing.T) { a, b := inlineHighlight("привет мир", "привет мор") assert.Contains(t, a, ansiBold) assert.Contains(t, b, ansiBold) // Common prefix "привет м" + common suffix "р" should be plain assertPlainContains(t, a, "привет м") assertPlainContains(t, b, "привет м") } // assertPlainContains checks that s contains substr in a position // not immediately preceded by an ANSI escape. func assertPlainContains(t *testing.T, s, substr string) { t.Helper() assert.Contains(t, s, substr, "string should contain %q", substr) } // --- integration: computeDiffOps + buildHunks round-trip consistency --- func TestDiffOps_AllOpsPreserveText(t *testing.T) { a := []string{"line1", "line2", "line3", "line4"} b := []string{"line1", "changed", "line3", "added", "line4"} ops := computeDiffOps(a, b) // Reconstruct A and B from ops var gotA, gotB []string for _, op := range ops { switch op.op { case opEqual: gotA = append(gotA, op.text) gotB = append(gotB, op.text) case opRemove: gotA = append(gotA, op.text) case opAdd: gotB = append(gotB, op.text) } } assert.Equal(t, a, gotA, "reconstructed A must match original") assert.Equal(t, b, gotB, "reconstructed B must match original") } func TestBuildHunks_AllChangedLinesPresent(t *testing.T) { a := strings.Split("a\nb\nc\nd\ne\nf\ng\nh\ni\nj", "\n") b := strings.Split("a\nB\nc\nd\ne\nf\ng\nH\ni\nj", "\n") ops := computeDiffOps(a, b) hunks := buildHunks(ops, 1) // Collect all changed texts from hunks var removed, added []string for _, h := range hunks { for _, dl := range h.lines { switch dl.op { case opRemove: removed = append(removed, dl.text) case opAdd: added = append(added, dl.text) } } } assert.Equal(t, []string{"b", "h"}, removed) assert.Equal(t, []string{"B", "H"}, added) }