package main import ( "strings" "testing" "github.com/charmbracelet/x/ansi" ) func TestParseCommentBodyExtractsSuggestionFences(t *testing.T) { body := "Use the clearer name:\n\n```suggestion:-0+0\n better_name = value\n```\n\nThis also matches the API." got := parseCommentBody(body) if len(got.Suggestions) != 1 || got.Suggestions[0] != " better_name = value" { t.Fatalf("suggestions = %#v", got.Suggestions) } if strings.Contains(got.Prose, "```") || !strings.Contains(got.Prose, "Use the clearer name") || !strings.Contains(got.Prose, "matches the API") { t.Fatalf("prose = %q", got.Prose) } } func TestReviewedSourceLinesUseHistoricalSelection(t *testing.T) { comment := ReviewComment{ DiffHunk: "@@ -38,5 +38,5 @@\n context\n old_name = value\n return old_name\n context\n context", OriginalStartLine: 39, OriginalLine: 40, } got := reviewedSourceLines(ReviewThread{DiffSide: "RIGHT"}, comment) want := []string{"old_name = value", "return old_name"} if strings.Join(got, "\n") != strings.Join(want, "\n") { t.Fatalf("reviewed source = %#v, want %#v", got, want) } } func TestNormalizeSuggestionPreservesRelativeIndent(t *testing.T) { removed, added := normalizeSuggestion( []string{" if old:", " run_old()"}, " if new:\n run_new()", ) if strings.Join(removed, "\n") != "if old:\n run_old()" { t.Fatalf("removed = %#v", removed) } if strings.Join(added, "\n") != "if new:\n run_new()" { t.Fatalf("added = %#v", added) } } func TestDetailRendersSuggestionAsRemovalAndAddition(t *testing.T) { m := NewApp(nil, "o", "r", false, 50, 10) m.width = 100 m.details = PRDetails{Threads: []ReviewThread{{ Path: "example.py", Line: 10, DiffSide: "RIGHT", Comments: []ReviewComment{{ Author: "reviewer", Body: "Use this instead:\n```suggestion\nnew_value = compute()\nreturn new_value\n```", DiffHunk: "@@ -10,2 +10,2 @@\nold_value = compute()\nreturn old_value", OriginalLine: 11, OriginalStartLine: 10, }}, }}} var ( removed, added int rendered strings.Builder ) for _, line := range m.detailLines(60) { rendered.WriteString(ansi.Strip(line.rail + line.fixed + line.text)) rendered.WriteByte('\n') switch line.suggestionChange { case '-': removed++ case '+': added++ } } if removed != 2 || added != 2 { t.Fatalf("suggestion rows: removed=%d added=%d\n%s", removed, added, rendered.String()) } if strings.Contains(rendered.String(), "```suggestion") || !strings.Contains(rendered.String(), "│ @reviewer") || !strings.Contains(rendered.String(), "│ suggested change") { t.Fatalf("suggestion was not rendered structurally:\n%s", rendered.String()) } } func TestSuggestionBackgroundIsDirectionalWithoutTextUnderline(t *testing.T) { removed := suggestionHighlight(" - ", "old", 12, '-') added := suggestionHighlight(" + ", "new", 12, '+') if !strings.Contains(removed, "\x1b[48;5;52m") || !strings.Contains(added, "\x1b[48;5;22m") { t.Fatalf("suggestion decorations missing: removed=%q added=%q", removed, added) } if strings.Contains(removed, "\x1b[4m") || strings.Contains(added, "\x1b[4m") || strings.Contains(removed, "\x1b[4;") || strings.Contains(added, "\x1b[4;") || strings.Contains(removed, "\x1b[58;") || strings.Contains(added, "\x1b[58;") { t.Fatal("suggestion rendering still enables terminal underlining") } if ansi.StringWidth(removed) != 12 || ansi.StringWidth(added) != 12 { t.Fatal("suggestion backgrounds do not fill the row") } if !strings.Contains(removed, "\x1b[0m\x1b[48;5;52m ") || !strings.Contains(added, "\x1b[0m\x1b[48;5;22m ") { t.Fatal("padded row remainder is still underlined") } } func TestMultipleSuggestionsStayInsideCommentBlock(t *testing.T) { m := NewApp(nil, "o", "r", false, 50, 10) m.width = 100 m.details = PRDetails{Threads: []ReviewThread{{ Path: "main.go", Line: 1, DiffSide: "RIGHT", Comments: []ReviewComment{{ Author: "alice", OriginalLine: 1, DiffHunk: "@@ -1 +1 @@\nold", Body: "```suggestion\nfirst\n```\n```suggestion\nsecond\n```", }}, }}} var rendered strings.Builder for _, line := range m.detailLines(60) { rendered.WriteString(ansi.Strip(line.rail + line.fixed + line.text)) rendered.WriteByte('\n') } if !strings.Contains(rendered.String(), "│ @alice") || !strings.Contains(rendered.String(), "│ suggested change 1/2") || !strings.Contains(rendered.String(), "│ suggested change 2/2") { t.Fatalf("suggestions escaped the comment block:\n%s", rendered.String()) } } func TestSuggestionGutterHasNoLeadingPadding(t *testing.T) { lines := wrapSuggestionLine("main.go", "replacement()", '+', fullCodeRange("replacement()"), 30) if len(lines) == 0 || ansi.Strip(lines[0].fixed) != "+ " { t.Fatalf("suggestion gutter = %q, want compact marker", ansi.Strip(lines[0].fixed)) } } func TestSuggestionChangedRangesAlignInsertedLines(t *testing.T) { removed := []string{"def setup():", " if ready:", " run()"} added := []string{"def setup():", " super().setup()", " if ready:", " run()"} removedRanges, addedRanges := suggestionChangedRanges(removed, added) for i, changed := range removedRanges { if changed.End != changed.Start { t.Fatalf("unchanged removed line %d marked as changed: %#v", i, changed) } } for _, index := range []int{0, 2, 3} { if changed := addedRanges[index]; changed.End != changed.Start { t.Fatalf("unchanged added line %d marked as changed: %#v", index, changed) } } if addedRanges[1] != fullCodeRange(added[1]) { t.Fatalf("inserted line range = %#v", addedRanges[1]) } } func TestSuggestionChangedRangeIsCharacterPrecise(t *testing.T) { removed, added := changedRange("value = old_name()", "value = new_name()") if got := "value = old_name()"[removed.Start:removed.End]; got != "old" { t.Fatalf("removed range selected %q", got) } if got := "value = new_name()"[added.Start:added.End]; got != "new" { t.Fatalf("added range selected %q", got) } } func TestChangedSnippetGetsDarkerBackground(t *testing.T) { code := highlightedSource("go", "value := oldName()") decorated := highlightCodeRange(code, codeRange{Start: 9, End: 16}, '-') if !strings.Contains(decorated, "\x1b[48;2;55;0;0m") { t.Fatalf("darker intra-line background missing: %q", decorated) } if ansi.Strip(decorated) != "value := oldName()" { t.Fatalf("highlight altered source: %q", ansi.Strip(decorated)) } }