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
| 1 | import { describe, expect, test, tier } from 'claude-code/testing' |
| 2 | |
| 3 | import Git from '../../../hooks/git' |
| 4 | import Parse from '../../../hooks/git/parse' |
| 5 | import Limits from '../../../hooks/limits' |
| 6 | import Fixtures from '../../fixtures' |
| 7 | |
| 8 | tier('builtin') |
| 9 | |
| 10 | describe('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 | }) |