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 Limits from '../../hooks/limits' |
| 5 | import Fixtures from '../fixtures' |
| 6 | |
| 7 | tier('builtin') |
| 8 | |
| 9 | describe('fetch-hunks', () => { |
| 10 | type Answer = (asked: readonly string[], nth: number) => Git.RunResult |
| 11 | |
| 12 | const rowOf = (path: string, kind: Partial<Git.FileStat> = {}) => ({ |
| 13 | path, |
| 14 | renamedFrom: null, |
| 15 | added: 1, |
| 16 | removed: 0, |
| 17 | isBinary: false, |
| 18 | isUntracked: false, |
| 19 | isPreSession: false, |
| 20 | ...kind, |
| 21 | }) |
| 22 | |
| 23 | const dataOf = ( |
| 24 | files: readonly Git.FileStat[], |
| 25 | basis: Partial<Git.DiffData> = {}, |
| 26 | ): Git.DiffData => ({ |
| 27 | repository: Fixtures.FETCH_REPOSITORY, |
| 28 | mode: 'uncommitted', |
| 29 | stats: { filesCount: files.length, linesAdded: 0, linesRemoved: 0 }, |
| 30 | files, |
| 31 | source: { kind: 'working-tree', base: 'HEAD' }, |
| 32 | isUnborn: false, |
| 33 | baseRef: 'HEAD', |
| 34 | stalePaths: [], |
| 35 | isUntrackedWithheld: false, |
| 36 | ...basis, |
| 37 | }) |
| 38 | |
| 39 | function gitOf(answer: Answer = Fixtures.hunksReadOf) { |
| 40 | const asks: (readonly string[])[] = [] |
| 41 | const argvs: (readonly string[])[] = [] |
| 42 | |
| 43 | const run: Git.GitRun = argv => { |
| 44 | const asked = argv.slice(argv.indexOf('--') + 1) |
| 45 | |
| 46 | argvs.push(argv) |
| 47 | asks.push(asked) |
| 48 | |
| 49 | return Promise.resolve(answer(asked, asks.length)) |
| 50 | } |
| 51 | |
| 52 | return { run, asks, argvs } |
| 53 | } |
| 54 | |
| 55 | const linesOf = (body: Git.FileHunks | null | undefined) => |
| 56 | body?.hunks.flatMap(hunk => hunk.lines) |
| 57 | |
| 58 | const FIFTY = Array.from({ length: Limits.MAX_FILES }, (_, at) => |
| 59 | rowOf(`src/file${at + Limits.MAX_FILES}.ts`), |
| 60 | ) |
| 61 | |
| 62 | const LONG = ['a', 'b', 'c'].map(name => |
| 63 | rowOf(name.repeat(Limits.MAX_PATHSPEC_CHARS / 2)), |
| 64 | ) |
| 65 | |
| 66 | test('fifty files are read by one child', async () => { |
| 67 | const git = gitOf() |
| 68 | const read = await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY) |
| 69 | |
| 70 | expect(git.asks).toEqual([FIFTY.map(file => file.path)]) |
| 71 | expect(read.size).toBe(Limits.MAX_FILES) |
| 72 | |
| 73 | expect( |
| 74 | FIFTY.every( |
| 75 | file => linesOf(read.get(file.path))?.[1] === `+in ${file.path}`, |
| 76 | ), |
| 77 | ).toBe(true) |
| 78 | }) |
| 79 | |
| 80 | test('the child keeps every pin of the read of one file', async () => { |
| 81 | const git = gitOf() |
| 82 | |
| 83 | await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY.slice(0, 1)) |
| 84 | |
| 85 | expect(git.argvs[0]?.slice(0, -1)).toEqual([ |
| 86 | '--literal-pathspecs', |
| 87 | '--no-optional-locks', |
| 88 | '-c', |
| 89 | 'diff.relative=false', |
| 90 | '-c', |
| 91 | 'core.quotePath=false', |
| 92 | 'diff', |
| 93 | '--no-ext-diff', |
| 94 | '--no-textconv', |
| 95 | '--ignore-submodules=dirty', |
| 96 | '--submodule=short', |
| 97 | '--no-renames', |
| 98 | '--src-prefix=a/', |
| 99 | '--dst-prefix=b/', |
| 100 | '--raw', |
| 101 | '-z', |
| 102 | '-p', |
| 103 | 'HEAD', |
| 104 | '--', |
| 105 | ]) |
| 106 | }) |
| 107 | |
| 108 | test('on an unborn HEAD the child reads what is staged', async () => { |
| 109 | const git = gitOf() |
| 110 | const files = [rowOf('edited-since.ts'), rowOf('staged.ts')] |
| 111 | |
| 112 | const read = await Git.fetchHunks( |
| 113 | git.run, |
| 114 | dataOf(files, { |
| 115 | isUnborn: true, |
| 116 | baseRef: '--cached', |
| 117 | stalePaths: ['edited-since.ts'], |
| 118 | }), |
| 119 | files, |
| 120 | ) |
| 121 | |
| 122 | expect(git.argvs[0]?.slice(-3)).toEqual(['--cached', '--', 'staged.ts']) |
| 123 | expect(read.get('edited-since.ts')).toEqual(Git.EMPTY_FILE_HUNKS) |
| 124 | expect(linesOf(read.get('staged.ts'))).toEqual([' one', '+in staged.ts']) |
| 125 | }) |
| 126 | |
| 127 | test('rows with no body are never named to git', async () => { |
| 128 | const git = gitOf() |
| 129 | |
| 130 | const files = [ |
| 131 | rowOf('app.ts'), |
| 132 | rowOf('logo.png', { isBinary: true }), |
| 133 | rowOf('moved.ts', { renamedFrom: 'old.ts' }), |
| 134 | rowOf('new.ts', { isUntracked: true }), |
| 135 | ] |
| 136 | |
| 137 | const read = await Git.fetchHunks(git.run, dataOf(files), files) |
| 138 | |
| 139 | expect(git.asks).toEqual([['app.ts']]) |
| 140 | |
| 141 | expect( |
| 142 | ['logo.png', 'moved.ts', 'new.ts'].map(path => read.get(path)), |
| 143 | ).toEqual([ |
| 144 | Git.EMPTY_FILE_HUNKS, |
| 145 | Git.EMPTY_FILE_HUNKS, |
| 146 | Git.EMPTY_FILE_HUNKS, |
| 147 | ]) |
| 148 | }) |
| 149 | |
| 150 | test('rows with no body alone start no child', async () => { |
| 151 | const git = gitOf() |
| 152 | const files = [rowOf('new.ts', { isUntracked: true })] |
| 153 | |
| 154 | await Git.fetchHunks(git.run, dataOf(files), files) |
| 155 | |
| 156 | expect(git.asks).toEqual([]) |
| 157 | }) |
| 158 | |
| 159 | test('every file but the last gains the closing line', async () => { |
| 160 | const git = gitOf() |
| 161 | const files = [rowOf('a.ts'), rowOf('b.ts')] |
| 162 | const read = await Git.fetchHunks(git.run, dataOf(files), files) |
| 163 | |
| 164 | expect(linesOf(read.get('a.ts'))).toEqual([' one', '+in a.ts', ' ']) |
| 165 | expect(linesOf(read.get('b.ts'))).toEqual([' one', '+in b.ts']) |
| 166 | }) |
| 167 | |
| 168 | test('paths past the budget are asked of a second child', async () => { |
| 169 | const git = gitOf() |
| 170 | const read = await Git.fetchHunks(git.run, dataOf(LONG), LONG) |
| 171 | const paths = LONG.map(file => file.path) |
| 172 | |
| 173 | expect(git.asks).toEqual([paths.slice(0, 2), paths.slice(2)]) |
| 174 | |
| 175 | expect(paths.map(path => linesOf(read.get(path))?.[1])).toEqual( |
| 176 | paths.map(path => `+in ${path}`), |
| 177 | ) |
| 178 | }) |
| 179 | |
| 180 | test('one path past the budget is still asked, alone', async () => { |
| 181 | const git = gitOf() |
| 182 | const files = [rowOf('d'.repeat(Limits.MAX_PATHSPEC_CHARS + 1)), rowOf('e')] |
| 183 | |
| 184 | await Git.fetchHunks(git.run, dataOf(files), files) |
| 185 | |
| 186 | expect(git.asks.map(asked => asked.length)).toEqual([1, 1]) |
| 187 | }) |
| 188 | |
| 189 | test('past the output cap the rest is asked again', async () => { |
| 190 | const files = [rowOf('a.ts'), rowOf('big.ts'), rowOf('c.ts'), rowOf('d.ts')] |
| 191 | |
| 192 | const git = gitOf((asked, nth) => { |
| 193 | const isFirst = nth === 1 |
| 194 | |
| 195 | return isFirst |
| 196 | ? Fixtures.ok( |
| 197 | Fixtures.hunksAnswerOf([ |
| 198 | ['a.ts', Fixtures.oneAddedOf('in a.ts')], |
| 199 | ['big.ts', Fixtures.longLineOf(Limits.HOST_OUTPUT_CAP_BYTES)], |
| 200 | ['c.ts', Fixtures.oneAddedOf('never arrived')], |
| 201 | ['d.ts', Fixtures.oneAddedOf('never arrived')], |
| 202 | ]).slice(0, Limits.HOST_OUTPUT_CAP_BYTES), |
| 203 | ) |
| 204 | : Fixtures.hunksReadOf(asked) |
| 205 | }) |
| 206 | |
| 207 | const read = await Git.fetchHunks(git.run, dataOf(files), files) |
| 208 | |
| 209 | expect(git.asks, 'the second list starts where the first stopped').toEqual([ |
| 210 | ['a.ts', 'big.ts', 'c.ts', 'd.ts'], |
| 211 | ['c.ts', 'd.ts'], |
| 212 | ]) |
| 213 | |
| 214 | expect(linesOf(read.get('a.ts'))).toEqual([' one', '+in a.ts', ' ']) |
| 215 | expect(read.get('big.ts')).toEqual(Git.LARGE_FILE_HUNKS) |
| 216 | expect(linesOf(read.get('c.ts'))).toEqual([' one', '+in c.ts', ' ']) |
| 217 | expect(linesOf(read.get('d.ts'))).toEqual([' one', '+in d.ts']) |
| 218 | }) |
| 219 | |
| 220 | test('a child that fails leaves every file unread', async () => { |
| 221 | const git = gitOf(() => Fixtures.GIT_TIMED_OUT) |
| 222 | const read = await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY) |
| 223 | |
| 224 | expect(git.asks).toHaveLength(1) |
| 225 | expect([...read.values()].every(body => body === null)).toBe(true) |
| 226 | expect(read.size).toBe(Limits.MAX_FILES) |
| 227 | }) |
| 228 | |
| 229 | test('a second child that fails keeps what the first read', async () => { |
| 230 | const git = gitOf((asked, nth) => { |
| 231 | const isFirst = nth === 1 |
| 232 | |
| 233 | return isFirst ? Fixtures.hunksReadOf(asked) : Fixtures.UNSCRIPTED |
| 234 | }) |
| 235 | |
| 236 | const read = await Git.fetchHunks(git.run, dataOf(LONG), LONG) |
| 237 | const [first, second, third] = LONG.map(file => read.get(file.path)) |
| 238 | |
| 239 | expect(git.asks).toHaveLength(2) |
| 240 | expect(linesOf(first)?.[0]).toBe(' one') |
| 241 | expect(linesOf(second)?.[0]).toBe(' one') |
| 242 | expect(third).toBeNull() |
| 243 | }) |
| 244 | |
| 245 | test('a second child is paired by its own list', async () => { |
| 246 | const git = gitOf((asked, nth) => { |
| 247 | const isFirst = nth === 1 |
| 248 | |
| 249 | return Fixtures.hunksReadOf(isFirst ? asked : asked.toReversed()) |
| 250 | }) |
| 251 | |
| 252 | const files = [...LONG, rowOf('z.ts')] |
| 253 | const read = await Git.fetchHunks(git.run, dataOf(files), files) |
| 254 | const [, , third] = LONG.map(file => file.path) |
| 255 | |
| 256 | expect(git.asks[1]).toEqual([third, 'z.ts']) |
| 257 | expect(linesOf(read.get(third ?? ''))?.[1]).toBe(`+in ${third}`) |
| 258 | expect(linesOf(read.get('z.ts'))).toEqual([' one', '+in z.ts']) |
| 259 | }) |
| 260 | |
| 261 | test('an answer whose names and patches disagree ends the read', async () => { |
| 262 | const git = gitOf(() => |
| 263 | Fixtures.ok( |
| 264 | Fixtures.hunksAnswerOf([ |
| 265 | ['a.ts', Fixtures.oneAddedOf('a'), 'diff --git a/o.ts b/o.ts'], |
| 266 | ['b.ts', Fixtures.oneAddedOf('b')], |
| 267 | ]), |
| 268 | ), |
| 269 | ) |
| 270 | |
| 271 | const files = [rowOf('a.ts'), rowOf('b.ts')] |
| 272 | const read = await Git.fetchHunks(git.run, dataOf(files), files) |
| 273 | |
| 274 | expect(git.asks, 'asked once, never again').toHaveLength(1) |
| 275 | |
| 276 | expect([...read]).toEqual([ |
| 277 | ['a.ts', null], |
| 278 | ['b.ts', null], |
| 279 | ]) |
| 280 | }) |
| 281 | }) |