spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / mods / diff / tests / git / parse / parse-file-diffs.test.ts
315 lines · 9.7 KBRaw
1import { describe, expect, test, tier } from 'claude-code/testing'
2
3import Git from '../../../hooks/git'
4import Parse from '../../../hooks/git/parse'
5import Limits from '../../../hooks/limits'
6import Fixtures from '../../fixtures'
7
8tier('builtin')
9
10describe('parse-file-diffs', () => {
11 const linesOf = (body: Git.FileHunks | undefined) =>
12 body?.hunks.flatMap(hunk => hunk.lines)
13
14 const cutAtCap = (printed: readonly Fixtures.PrintedFile[]) =>
15 Fixtures.hunksAnswerOf(printed).slice(0, Limits.HOST_OUTPUT_CAP_BYTES)
16
17 test('an empty answer: every file asked reads no hunks', () => {
18 expect([...Parse.parseFileDiffs('', ['a.ts', 'b.ts'])]).toEqual([
19 ['a.ts', Git.EMPTY_FILE_HUNKS],
20 ['b.ts', Git.EMPTY_FILE_HUNKS],
21 ])
22 })
23
24 test('each unusual name holds its own patch', () => {
25 expect(
26 [
27 ...Parse.parseFileDiffs(
28 Fixtures.hunksAnswerOf(Fixtures.UNUSUAL_NAMES),
29 Fixtures.UNUSUAL_NAMES.map(([path]) => path),
30 ),
31 ].map(([path, body]) => [path, linesOf(body)]),
32 ).toEqual(
33 Fixtures.UNUSUAL_NAMES.map(([path, body]) => [
34 path,
35 body.split('\n').slice(1, -1),
36 ]),
37 )
38 })
39
40 test('the order asked in does not move a patch', () => {
41 const read = Parse.parseFileDiffs(
42 Fixtures.hunksAnswerOf([
43 ['a.ts', Fixtures.oneAddedOf('first')],
44 ['b.ts', Fixtures.oneAddedOf('next')],
45 ]),
46 ['b.ts', 'a.ts'],
47 )
48
49 expect(linesOf(read.get('a.ts'))).toEqual([' one', '+first'])
50 expect(linesOf(read.get('b.ts'))).toEqual([' one', '+next'])
51 })
52
53 test('content that looks like an opening line stays content', () => {
54 const read = Parse.parseFileDiffs(
55 Fixtures.hunksAnswerOf([
56 [
57 'a.ts',
58 '@@ -1,2 +1,2 @@\n diff --git a/b.ts b/b.ts\n' +
59 '-diff --git a/x b/x\n+diff --git a/y b/y\n',
60 ],
61 ['b.ts', Fixtures.oneAddedOf('its own')],
62 ]),
63 ['a.ts', 'b.ts'],
64 )
65
66 expect(linesOf(read.get('a.ts'))).toEqual([
67 ' diff --git a/b.ts b/b.ts',
68 '-diff --git a/x b/x',
69 '+diff --git a/y b/y',
70 ])
71
72 expect(linesOf(read.get('b.ts'))).toEqual([' one', '+its own'])
73 })
74
75 test('a file turned link: its two patches are one file', () => {
76 const read = Parse.parseFileDiffs(
77 Fixtures.hunksAnswerOf([
78 ['a.ts', Fixtures.oneAddedOf('before')],
79 [
80 'link',
81 'deleted file mode 100644\n@@ -1 +0,0 @@\n-one\n' +
82 'diff --git a/link b/link\nnew file mode 120000\n' +
83 '@@ -0,0 +1 @@\n+a.ts\n',
84 ],
85 ['z.ts', Fixtures.oneAddedOf('after')],
86 ]),
87 ['a.ts', 'link', 'z.ts'],
88 )
89
90 expect(linesOf(read.get('a.ts'))).toEqual([' one', '+before'])
91 expect(linesOf(read.get('link'))).toEqual(['-one', '+a.ts'])
92 expect(linesOf(read.get('z.ts'))).toEqual([' one', '+after'])
93 })
94
95 test('a conflicted file read against HEAD is one more file', () => {
96 const read = Parse.parseFileDiffs(
97 Fixtures.hunksAnswerOf([
98 ['a.ts', Fixtures.oneAddedOf('before')],
99 [
100 'both changed.ts',
101 '@@ -1 +1,5 @@\n+<<<<<<< ours\n one\n+=======\n+theirs\n+>>>>>>> ' +
102 'theirs\n',
103 ],
104 ['gone on our side.ts', '@@ -0,0 +1 @@\n+theirs\n'],
105 ['z.ts', Fixtures.oneAddedOf('after')],
106 ]).replace(
107 Fixtures.rawRecordOf('gone on our side.ts'),
108 Fixtures.rawRecordOf('gone on our side.ts').replace(' M\0', ' A\0'),
109 ),
110 ['a.ts', 'both changed.ts', 'gone on our side.ts', 'z.ts'],
111 )
112
113 expect(linesOf(read.get('a.ts'))).toEqual([' one', '+before'])
114
115 expect(linesOf(read.get('both changed.ts'))).toEqual([
116 '+<<<<<<< ours',
117 ' one',
118 '+=======',
119 '+theirs',
120 '+>>>>>>> theirs',
121 ])
122
123 expect(linesOf(read.get('gone on our side.ts'))).toEqual(['+theirs'])
124 expect(linesOf(read.get('z.ts'))).toEqual([' one', '+after'])
125 })
126
127 test('an unmerged path has no patch and reads no hunks', () => {
128 const read = Parse.parseFileDiffs(
129 Fixtures.hunksAnswerOf([
130 ['a.ts', `${Fixtures.oneAddedOf('before')}* Unmerged path c.ts\n`],
131 ['z.ts', Fixtures.oneAddedOf('after')],
132 ]).replace(
133 Fixtures.rawRecordOf('z.ts'),
134 Fixtures.rawRecordOf('c.ts').replace(' M\0', ' U\0') +
135 Fixtures.rawRecordOf('z.ts'),
136 ),
137 ['a.ts', 'c.ts', 'z.ts'],
138 )
139
140 expect(linesOf(read.get('a.ts'))).toEqual([' one', '+before'])
141 expect(read.get('c.ts')).toEqual(Git.EMPTY_FILE_HUNKS)
142 expect(linesOf(read.get('z.ts'))).toEqual([' one', '+after'])
143 })
144
145 test('a file git no longer lists reads no hunks', () => {
146 const read = Parse.parseFileDiffs(
147 Fixtures.hunksAnswerOf([['kept.ts', Fixtures.oneAddedOf('changed')]]),
148 ['gone.ts', 'kept.ts'],
149 )
150
151 expect(read.get('gone.ts')).toEqual(Git.EMPTY_FILE_HUNKS)
152 expect(linesOf(read.get('kept.ts'))).toEqual([' one', '+changed'])
153 })
154
155 test('a listed file nobody asked for is left out', () => {
156 const read = Parse.parseFileDiffs(
157 Fixtures.hunksAnswerOf([
158 ['made', '@@ -1 +0,0 @@\n-a file\n'],
159 ['made/inside.ts', '@@ -0,0 +1 @@\n+now a folder\n'],
160 ]),
161 ['made'],
162 )
163
164 expect([...read.keys()]).toEqual(['made'])
165 expect(linesOf(read.get('made'))).toEqual(['-a file'])
166 })
167
168 test('with no empty field before the patches it reads the same', () => {
169 expect(
170 linesOf(
171 Parse.parseFileDiffs(
172 Fixtures.hunksAnswerOf([
173 ['a.ts', Fixtures.oneAddedOf('unparted')],
174 ]).replace('\0\0', '\0'),
175 ['a.ts'],
176 ).get('a.ts'),
177 ),
178 ).toEqual([' one', '+unparted'])
179 })
180
181 test('fewer patches than names settle nothing', () => {
182 expect(
183 Parse.parseFileDiffs(
184 Fixtures.rawRecordOf('a.ts') +
185 Fixtures.hunksAnswerOf([['b.ts', Fixtures.oneAddedOf('whose')]]),
186 ['a.ts', 'b.ts'],
187 ).size,
188 ).toBe(0)
189 })
190
191 test('a patch opened under another name settles nothing', () => {
192 expect(
193 Parse.parseFileDiffs(
194 Fixtures.hunksAnswerOf([
195 ['a.ts', Fixtures.oneAddedOf('here'), 'diff --git a/b.ts b/b.ts'],
196 ['b.ts', Fixtures.oneAddedOf('there'), 'diff --git a/a.ts b/a.ts'],
197 ]),
198 ['a.ts', 'b.ts'],
199 ).size,
200 ).toBe(0)
201 })
202
203 test('patches git coloured: no hunks, as one file read alone', () => {
204 expect([
205 ...Parse.parseFileDiffs(
206 Fixtures.hunksAnswerOf([
207 [
208 'a.ts',
209 '\u001b[36m@@ -1 +1,2 @@\u001b[m\n one\n\u001b[32m+two\u001b[m\n',
210 '\u001b[1mdiff --git a/a.ts b/a.ts\u001b[m',
211 ],
212 ]),
213 ['a.ts'],
214 ),
215 ]).toEqual([['a.ts', Git.EMPTY_FILE_HUNKS]])
216 })
217
218 test('the line cap and the size cap hold file by file', () => {
219 const long = Array.from(
220 { length: Limits.MAX_LINES_PER_FILE + Fixtures.PAST_THE_CAP },
221 (_, at) => `+${at}`,
222 ).join('\n')
223
224 const read = Parse.parseFileDiffs(
225 Fixtures.hunksAnswerOf([
226 ['large.ts', Fixtures.longLineOf(Limits.MAX_DIFF_BYTES)],
227 ['long.ts', `@@ -0,0 +1,405 @@\n${long}\n`],
228 ['small.ts', Fixtures.oneAddedOf('whole')],
229 ]),
230 ['large.ts', 'long.ts', 'small.ts'],
231 )
232
233 expect(read.get('large.ts')).toEqual(Git.LARGE_FILE_HUNKS)
234 expect(read.get('long.ts')?.isTruncated).toBe(true)
235
236 expect(linesOf(read.get('long.ts'))).toHaveLength(Limits.MAX_LINES_PER_FILE)
237
238 expect(read.get('small.ts')?.isTruncated).toBe(false)
239 expect(linesOf(read.get('small.ts'))).toEqual([' one', '+whole'])
240 })
241
242 test('a file at the size cap to the character is not large', () => {
243 const room =
244 Limits.MAX_DIFF_BYTES -
245 Fixtures.hunksAnswerOf([['edge.ts', Fixtures.longLineOf(0)]]).replace(
246 /^.*\0/s,
247 '',
248 ).length
249
250 const largePast = (extra: number) =>
251 Parse.parseFileDiffs(
252 Fixtures.hunksAnswerOf([
253 ['edge.ts', Fixtures.longLineOf(room + extra)],
254 ['next.ts', Fixtures.oneAddedOf('after it')],
255 ]),
256 ['edge.ts', 'next.ts'],
257 ).get('edge.ts')?.isLarge
258
259 expect(largePast(0)).toBe(false)
260 expect(largePast(1)).toBe(true)
261 })
262
263 test('cut by the host: whole files settle, the rest wait', () => {
264 const read = Parse.parseFileDiffs(
265 cutAtCap([
266 ['a.ts', Fixtures.oneAddedOf('whole')],
267 ['big.ts', Fixtures.longLineOf(Limits.MAX_DIFF_BYTES)],
268 ['cut.ts', Fixtures.longLineOf(Limits.HOST_OUTPUT_CAP_BYTES)],
269 ['unseen.ts', Fixtures.oneAddedOf('never arrived')],
270 ]),
271 ['a.ts', 'big.ts', 'cut.ts', 'unseen.ts'],
272 )
273
274 expect([...read.keys()]).toEqual(['a.ts', 'big.ts', 'cut.ts'])
275 expect(linesOf(read.get('a.ts'))).toEqual([' one', '+whole'])
276 expect(read.get('big.ts')).toEqual(Git.LARGE_FILE_HUNKS)
277
278 expect(read.get('cut.ts'), 'what arrived of it is already large').toEqual(
279 Git.LARGE_FILE_HUNKS,
280 )
281 })
282
283 test('cut by the host inside a small file: that file waits', () => {
284 expect([
285 ...Parse.parseFileDiffs(
286 cutAtCap([
287 [
288 'big.ts',
289 Fixtures.longLineOf(
290 Limits.HOST_OUTPUT_CAP_BYTES - Limits.MAX_DIFF_BYTES / 2,
291 ),
292 ],
293 ['cut.ts', Fixtures.longLineOf(Limits.MAX_DIFF_BYTES)],
294 ['unseen.ts', Fixtures.oneAddedOf('never arrived')],
295 ]),
296 ['big.ts', 'cut.ts', 'unseen.ts'],
297 ),
298 ]).toEqual([['big.ts', Git.LARGE_FILE_HUNKS]])
299 })
300
301 test('cut by the host: a file git did not list reads no hunks', () => {
302 expect([
303 ...Parse.parseFileDiffs(
304 cutAtCap([
305 ['cut.ts', Fixtures.longLineOf(Limits.HOST_OUTPUT_CAP_BYTES)],
306 ['unseen.ts', Fixtures.oneAddedOf('never arrived')],
307 ]),
308 ['cut.ts', 'gone.ts', 'unseen.ts'],
309 ),
310 ]).toEqual([
311 ['cut.ts', Git.LARGE_FILE_HUNKS],
312 ['gone.ts', Git.EMPTY_FILE_HUNKS],
313 ])
314 })
315})