1package chat
2
3import (
4 "strings"
5 "testing"
6)
7
8func TestLooksLikeDiff(t *testing.T) {
9 t.Parallel()
10
11 tests := []struct {
12 name string
13 content string
14 want bool
15 }{
16 {
17 name: "simple unified diff",
18 content: `diff --git a/main.go b/main.go
19--- a/main.go
20+++ b/main.go
21@@ -1,5 +1,6 @@
22 package main
23
24+import "fmt"
25+
26 func main() {
27- println("hello")
28+ fmt.Println("hello")
29 }
30`,
31 want: true,
32 },
33 {
34 name: "plain text",
35 content: "This is just some plain text with no diff markers.",
36 want: false,
37 },
38 {
39 name: "empty string",
40 content: "",
41 want: false,
42 },
43 {
44 name: "markdown with headers",
45 content: `# Title
46
47Some content here.
48
49## Subtitle
50
51More content with **bold** text.
52`,
53 want: false,
54 },
55 {
56 name: "diff with mixed content",
57 content: `diff --git a/file.txt b/file.txt
58--- a/file.txt
59+++ b/file.txt
60@@ -1 +1 @@
61-old line
62+new line
63`,
64 want: true,
65 },
66 {
67 name: "only plus/minus without hunk or headers",
68 content: `Hello world
69---
70This is not really a diff
71Just some text with a few symbols
72+ another line
73More regular content here
74And even more content
75`,
76 want: false,
77 },
78 {
79 name: "GitHub PR diff format",
80 content: `diff --git a/src/app.ts b/src/app.ts
81index abc1234..def5678 100644
82--- a/src/app.ts
83+++ b/src/app.ts
84@@ -10,6 +10,8 @@ function handleRequest() {
85 const data = getData();
86+ validate(data);
87+ log(data);
88 return process(data);
89 }
90`,
91 want: true,
92 },
93 {
94 name: "non-git unified patch with hunk and headers",
95 content: `--- a/old.c
96+++ b/old.c
97@@ -1,3 +1,4 @@
98 #include <stdio.h>
99-int main() {
100+int main(int argc, char **argv) {
101 return 0;
102 }
103`,
104 want: true,
105 },
106 {
107 name: "file headers without hunk markers",
108 content: `--- a/somefile.txt
109+++ b/somefile.txt
110Just some content here
111No hunk markers at all
112`,
113 want: false,
114 },
115 {
116 name: "hunk markers without file headers",
117 content: `@@ -1,3 +1,4 @@
118 some line
119-another line
120+changed line
121`,
122 want: false,
123 },
124 {
125 name: "markdown list with plus signs",
126 content: `- Item one
127- Item two
128+ Bonus item
129- Item three
130`,
131 want: false,
132 },
133 }
134
135 for _, tt := range tests {
136 t.Run(tt.name, func(t *testing.T) {
137 t.Parallel()
138 got := looksLikeDiff(tt.content)
139 if got != tt.want {
140 t.Errorf("looksLikeDiff() = %v, want %v", got, tt.want)
141 }
142 })
143 }
144}
145
146func TestParseUnifiedDiff(t *testing.T) {
147 t.Parallel()
148
149 tests := []struct {
150 name string
151 input string
152 want []parsedDiffFile
153 }{
154 {
155 name: "simple diff with additions and removals",
156 input: `diff --git a/main.go b/main.go
157--- a/main.go
158+++ b/main.go
159@@ -1,5 +1,6 @@
160 package main
161
162+import "fmt"
163+
164 func main() {
165- println("hello")
166+ fmt.Println("hello")
167 }
168`,
169 want: []parsedDiffFile{
170 {
171 path: "main.go",
172 before: "package main\n\nfunc main() {\n println(\"hello\")\n}",
173 after: "package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"hello\")\n}",
174 },
175 },
176 },
177 {
178 name: "new file creation",
179 input: `diff --git a/newfile.go b/newfile.go
180new file mode 100644
181--- /dev/null
182+++ b/newfile.go
183@@ -0,0 +1,3 @@
184+package main
185+
186+func main() {}
187`,
188 want: []parsedDiffFile{
189 {
190 path: "newfile.go",
191 before: "",
192 after: "package main\n\nfunc main() {}",
193 },
194 },
195 },
196 {
197 name: "file deletion",
198 input: `diff --git a/oldfile.go b/oldfile.go
199deleted file mode 100644
200--- a/oldfile.go
201+++ /dev/null
202@@ -1,3 +0,0 @@
203-package main
204-
205-func main() {}
206`,
207 want: []parsedDiffFile{
208 {
209 path: "oldfile.go",
210 before: "package main\n\nfunc main() {}",
211 after: "",
212 },
213 },
214 },
215 {
216 name: "non-diff content",
217 input: "Just some regular text",
218 want: nil,
219 },
220 {
221 name: "diff with timestamp in header",
222 input: `diff --git a/config.yml b/config.yml
223--- a/config.yml 2024-01-15 10:30:00
224+++ b/config.yml 2024-01-15 10:31:00
225@@ -1,3 +1,4 @@
226 name: myapp
227-version: 1.0
228+version: 1.1
229+debug: true
230`,
231 want: []parsedDiffFile{
232 {
233 path: "config.yml",
234 before: "name: myapp\nversion: 1.0",
235 after: "name: myapp\nversion: 1.1\ndebug: true",
236 },
237 },
238 },
239 {
240 name: "multi-file diff",
241 input: `diff --git a/one.txt b/one.txt
242--- a/one.txt
243+++ b/one.txt
244@@ -1,3 +1,3 @@
245 line one
246-line two
247+line two updated
248 line three
249diff --git a/two.txt b/two.txt
250--- a/two.txt
251+++ b/two.txt
252@@ -1,2 +1,3 @@
253 alpha
254+beta
255 gamma
256`,
257 want: []parsedDiffFile{
258 {
259 path: "one.txt",
260 before: "line one\nline two\nline three",
261 after: "line one\nline two updated\nline three",
262 },
263 {
264 path: "two.txt",
265 before: "alpha\ngamma",
266 after: "alpha\nbeta\ngamma",
267 },
268 },
269 },
270 {
271 name: "non-git unified patch",
272 input: `--- old.c
273+++ old.c
274@@ -1,3 +1,4 @@
275 #include <stdio.h>
276-int main() {
277+int main(int argc, char **argv) {
278 return 0;
279 }
280`,
281 want: []parsedDiffFile{
282 {
283 path: "old.c",
284 before: "#include <stdio.h>\nint main() {\n return 0;\n}",
285 after: "#include <stdio.h>\nint main(int argc, char **argv) {\n return 0;\n}",
286 },
287 },
288 },
289 {
290 name: "non-git new file from /dev/null",
291 input: `--- /dev/null
292+++ newfile.txt
293@@ -0,0 +1,2 @@
294+hello
295+world
296`,
297 want: []parsedDiffFile{
298 {
299 path: "newfile.txt",
300 before: "",
301 after: "hello\nworld",
302 },
303 },
304 },
305 {
306 name: "non-git new file with only +++ header",
307 input: `+++ brand_new.go
308@@ -0,0 +1,3 @@
309+package main
310+
311+func main() {}
312`,
313 want: []parsedDiffFile{
314 {
315 path: "brand_new.go",
316 before: "",
317 after: "package main\n\nfunc main() {}",
318 },
319 },
320 },
321 {
322 name: "multi-hunk single file",
323 input: `diff --git a/big.go b/big.go
324--- a/big.go
325+++ b/big.go
326@@ -1,4 +1,5 @@
327 package main
328+import "os"
329
330 func init() {
331@@ -10,3 +11,3 @@
332- println("done")
333+ fmt.Println("done")
334 }
335`,
336 want: []parsedDiffFile{
337 {
338 path: "big.go",
339 before: "package main\n\nfunc init() {\n println(\"done\")\n}",
340 after: "package main\nimport \"os\"\n\nfunc init() {\n fmt.Println(\"done\")\n}",
341 },
342 },
343 },
344 {
345 name: "hunk content starting with header-like prefixes",
346 input: `diff --git a/file.txt b/file.txt
347--- a/file.txt
348+++ b/file.txt
349@@ -1,3 +1,3 @@
350---- tricky
351++++ newer
352 keep
353`,
354 want: []parsedDiffFile{
355 {
356 path: "file.txt",
357 before: "--- tricky\nkeep",
358 after: "+++ newer\nkeep",
359 },
360 },
361 },
362 }
363
364 for _, tt := range tests {
365 t.Run(tt.name, func(t *testing.T) {
366 t.Parallel()
367 got := parseUnifiedDiff(tt.input)
368 if len(got) != len(tt.want) {
369 t.Errorf("parseUnifiedDiff() returned %d files, want %d", len(got), len(tt.want))
370 return
371 }
372 for i, w := range tt.want {
373 if got[i].path != w.path {
374 t.Errorf("parseUnifiedDiff()[%d].path = %q, want %q", i, got[i].path, w.path)
375 }
376 if got[i].before != w.before {
377 t.Errorf("parseUnifiedDiff()[%d].before = %q, want %q", i, got[i].before, w.before)
378 }
379 if got[i].after != w.after {
380 t.Errorf("parseUnifiedDiff()[%d].after = %q, want %q", i, got[i].after, w.after)
381 }
382 }
383 })
384 }
385}
386
387func TestLooksLikeDiffVersusMarkdown(t *testing.T) {
388 t.Parallel()
389
390 // A unified diff should be detected as a diff, not markdown,
391 // even though it contains "-" which could match markdown patterns.
392 diffContent := strings.Join([]string{
393 "diff --git a/README.md b/README.md",
394 "--- a/README.md",
395 "+++ b/README.md",
396 "@@ -1,3 +1,3 @@",
397 " # Title",
398 "-Old subtitle",
399 "+New subtitle",
400 " Some content",
401 }, "\n")
402
403 if !looksLikeDiff(diffContent) {
404 t.Error("looksLikeDiff() should detect unified diff")
405 }
406}