@@ -0,0 +1,406 @@
1+package chat
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+func 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+
47+Some content here.
48+
49+## Subtitle
50+
51+More 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+---
70+This is not really a diff
71+Just some text with a few symbols
72++ another line
73+More regular content here
74+And 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
81+index 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
110+Just some content here
111+No 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+
146+func 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
180+new 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
199+deleted 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
249+diff --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+
387+func 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+}