返回 DeepSeek-Reasonix
transcript_copy_gutter_test.go
根目录 / internal / cli / transcript_copy_gutter_test.go
1 package cli
2
3 import (
4 "bytes"
5 "strings"
6 "testing"
7 "unicode/utf8"
8
9 "github.com/charmbracelet/colorprofile"
10 "github.com/charmbracelet/x/ansi"
11
12 "reasonix/internal/provider"
13 )
14
15 func transcriptSelectionModel(source transcriptSource) chatTUI {
16 m := newTestChatTUI()
17 contentWidth := transcriptContentWidth(m.width, m.nativeScrollback)
18 m.viewport.SetWidth(contentWidth)
19 rendered := m.renderTranscriptSource(source, m.width)
20 m.transcript = []string{rendered}
21 m.transcriptSources = []transcriptSource{source}
22 m.wrappedLines = wrapBlockLines(rendered, contentWidth)
23 return m
24 }
25
26 func transcriptLineContaining(t *testing.T, lines []string, needle string) int {
27 t.Helper()
28 for i, line := range lines {
29 if strings.Contains(ansi.Strip(line), needle) {
30 return i
31 }
32 }
33 t.Fatalf("rendered transcript did not contain %q:\n%s", needle, ansi.Strip(strings.Join(lines, "\n")))
34 return -1
35 }
36
37 func selectTranscriptLines(m *chatTUI, first, last int) string {
38 m.sel = selection{
39 active: true,
40 anchor: selPos{line: first},
41 head: selPos{line: last, col: ansi.StringWidth(m.wrappedLines[last])},
42 }
43 return m.selectedText()
44 }
45
46 func TestSelectedTextStripsRenderedMarkdownGutters(t *testing.T) {
47 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
48 activeColorProfile = colorprofile.TrueColor
49 configureCLITheme("dark")
50
51 m := transcriptSelectionModel(transcriptSource{
52 kind: transcriptSourceMarkdown,
53 raw: "```go\nconst x = 1\nif x > 0 {\n```",
54 })
55 first := transcriptLineContaining(t, m.wrappedLines, "│ const x = 1")
56 last := transcriptLineContaining(t, m.wrappedLines, "│ if x > 0 {")
57 if got, want := selectTranscriptLines(&m, first, last), "const x = 1\nif x > 0 {"; got != want {
58 t.Fatalf("fenced selection = %q, want %q", got, want)
59 }
60 plain := ansi.Strip(m.wrappedLines[first])
61 contentCol := ansi.StringWidth(strings.Split(plain, "const x")[0])
62 m.sel = selection{
63 active: true,
64 anchor: selPos{line: first, col: contentCol},
65 head: selPos{line: first, col: contentCol + ansi.StringWidth("const x")},
66 }
67 if got, want := m.selectedText(), "const x"; got != want {
68 t.Fatalf("mid-line fenced selection = %q, want %q", got, want)
69 }
70
71 m = transcriptSelectionModel(transcriptSource{kind: transcriptSourceMarkdown, raw: "> quoted"})
72 line := transcriptLineContaining(t, m.wrappedLines, "▎ quoted")
73 if got, want := selectTranscriptLines(&m, line, line), "quoted"; got != want {
74 t.Fatalf("blockquote selection = %q, want %q", got, want)
75 }
76 }
77
78 func TestSelectedTextPreservesLiteralGutterGlyphs(t *testing.T) {
79 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
80 activeColorProfile = colorprofile.NoTTY
81 configureCLITheme("dark")
82
83 for _, literal := range []string{"│ literal", "▎ literal", "⎿ literal", "| table | row |"} {
84 m := transcriptSelectionModel(transcriptSource{kind: transcriptSourceMarkdown, raw: literal})
85 line := transcriptLineContaining(t, m.wrappedLines, literal)
86 plain := strings.TrimRight(ansi.Strip(m.wrappedLines[line]), " ")
87 if got := selectTranscriptLines(&m, line, line); got != plain {
88 t.Fatalf("full-line selection for %q = %q, want displayed %q", literal, got, plain)
89 }
90 start := ansi.StringWidth(strings.Split(plain, literal)[0])
91 m.sel = selection{
92 active: true,
93 anchor: selPos{line: line, col: start},
94 head: selPos{line: line, col: start + ansi.StringWidth(literal)},
95 }
96 if got := m.selectedText(); got != literal {
97 t.Fatalf("literal selection for %q = %q", literal, got)
98 }
99 }
100
101 m := newTestChatTUI()
102 m.wrappedLines = []string{" │ fallback literal"}
103 if got, want := selectTranscriptLines(&m, 0, 0), " │ fallback literal"; got != want {
104 t.Fatalf("display fallback = %q, want %q", got, want)
105 }
106 }
107
108 func TestSelectedTextStripsWholeConnectorGutter(t *testing.T) {
109 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
110 activeColorProfile = colorprofile.NoTTY
111 configureCLITheme("dark")
112
113 m := newTestChatTUI()
114 contentWidth := transcriptContentWidth(m.width, m.nativeScrollback)
115 m.viewport.SetWidth(contentWidth)
116 m.beginToolRunning("shell-copy-test")
117 m.streamToolOutput("shell-copy-test", "first\nsecond\n│ literal output")
118 m.wrappedLines = wrapBlockLines(m.transcript[0], contentWidth)
119 if got, want := selectTranscriptLines(&m, 0, 2), "first\nsecond\n│ literal output"; got != want {
120 t.Fatalf("connector selection = %q, want %q", got, want)
121 }
122 }
123
124 func TestSelectedTextStripsReplayReasoningConnector(t *testing.T) {
125 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
126 activeColorProfile = colorprofile.NoTTY
127 configureCLITheme("dark")
128
129 m := transcriptSelectionModel(transcriptSource{
130 kind: transcriptSourceReplayBundle,
131 history: []provider.Message{{
132 Role: provider.RoleAssistant, ReasoningContent: "first\nsecond",
133 }},
134 })
135 first := transcriptLineContaining(t, m.wrappedLines, "⎿ first")
136 last := transcriptLineContaining(t, m.wrappedLines, "second")
137 if got, want := selectTranscriptLines(&m, first, last), "first\nsecond"; got != want {
138 t.Fatalf("replayed reasoning selection = %q, want %q", got, want)
139 }
140 }
141
142 func TestNativeClipboardPassesCJKUTF8Unchanged(t *testing.T) {
143 t.Setenv("SSH_CONNECTION", "")
144 t.Setenv("SSH_CLIENT", "")
145 t.Setenv("SSH_TTY", "")
146 previous := writeNativeClipboardText
147 defer func() { writeNativeClipboardText = previous }()
148
149 want := "这是中文复制验证 │ ▎ ⎿"
150 var written []byte
151 writeNativeClipboardText = func(text string) error {
152 written = append([]byte(nil), []byte(text)...)
153 return nil
154 }
155 msg := copyToClipboard(want)().(clipboardCopyMsg)
156 if msg.err != nil || msg.osc52 {
157 t.Fatalf("native clipboard result = %+v", msg)
158 }
159 if !utf8.Valid(written) || !bytes.Equal(written, []byte(want)) {
160 t.Fatalf("native clipboard bytes = %q, want unchanged UTF-8 %q", written, []byte(want))
161 }
162 }
163
163 lines GO