mirror of
https://github.com/anthropics/claude-code.git
synced 2026-10-20 06:09:43 +08:00
282 lines
8.2 KiB
TypeScript
282 lines
8.2 KiB
TypeScript
import { describe, expect, test, tier } from 'claude-code/testing'
|
|
|
|
import Git from '../../hooks/git'
|
|
import Limits from '../../hooks/limits'
|
|
import Fixtures from '../fixtures'
|
|
|
|
tier('builtin')
|
|
|
|
describe('fetch-hunks', () => {
|
|
type Answer = (asked: readonly string[], nth: number) => Git.RunResult
|
|
|
|
const rowOf = (path: string, kind: Partial<Git.FileStat> = {}) => ({
|
|
path,
|
|
renamedFrom: null,
|
|
added: 1,
|
|
removed: 0,
|
|
isBinary: false,
|
|
isUntracked: false,
|
|
isPreSession: false,
|
|
...kind,
|
|
})
|
|
|
|
const dataOf = (
|
|
files: readonly Git.FileStat[],
|
|
basis: Partial<Git.DiffData> = {},
|
|
): Git.DiffData => ({
|
|
repository: Fixtures.FETCH_REPOSITORY,
|
|
mode: 'uncommitted',
|
|
stats: { filesCount: files.length, linesAdded: 0, linesRemoved: 0 },
|
|
files,
|
|
source: { kind: 'working-tree', base: 'HEAD' },
|
|
isUnborn: false,
|
|
baseRef: 'HEAD',
|
|
stalePaths: [],
|
|
isUntrackedWithheld: false,
|
|
...basis,
|
|
})
|
|
|
|
function gitOf(answer: Answer = Fixtures.hunksReadOf) {
|
|
const asks: (readonly string[])[] = []
|
|
const argvs: (readonly string[])[] = []
|
|
|
|
const run: Git.GitRun = argv => {
|
|
const asked = argv.slice(argv.indexOf('--') + 1)
|
|
|
|
argvs.push(argv)
|
|
asks.push(asked)
|
|
|
|
return Promise.resolve(answer(asked, asks.length))
|
|
}
|
|
|
|
return { run, asks, argvs }
|
|
}
|
|
|
|
const linesOf = (body: Git.FileHunks | null | undefined) =>
|
|
body?.hunks.flatMap(hunk => hunk.lines)
|
|
|
|
const FIFTY = Array.from({ length: Limits.MAX_FILES }, (_, at) =>
|
|
rowOf(`src/file${at + Limits.MAX_FILES}.ts`),
|
|
)
|
|
|
|
const LONG = ['a', 'b', 'c'].map(name =>
|
|
rowOf(name.repeat(Limits.MAX_PATHSPEC_CHARS / 2)),
|
|
)
|
|
|
|
test('fifty files are read by one child', async () => {
|
|
const git = gitOf()
|
|
const read = await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY)
|
|
|
|
expect(git.asks).toEqual([FIFTY.map(file => file.path)])
|
|
expect(read.size).toBe(Limits.MAX_FILES)
|
|
|
|
expect(
|
|
FIFTY.every(
|
|
file => linesOf(read.get(file.path))?.[1] === `+in ${file.path}`,
|
|
),
|
|
).toBe(true)
|
|
})
|
|
|
|
test('the child keeps every pin of the read of one file', async () => {
|
|
const git = gitOf()
|
|
|
|
await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY.slice(0, 1))
|
|
|
|
expect(git.argvs[0]?.slice(0, -1)).toEqual([
|
|
'--literal-pathspecs',
|
|
'--no-optional-locks',
|
|
'-c',
|
|
'diff.relative=false',
|
|
'-c',
|
|
'core.quotePath=false',
|
|
'diff',
|
|
'--no-ext-diff',
|
|
'--no-textconv',
|
|
'--ignore-submodules=dirty',
|
|
'--submodule=short',
|
|
'--no-renames',
|
|
'--src-prefix=a/',
|
|
'--dst-prefix=b/',
|
|
'--raw',
|
|
'-z',
|
|
'-p',
|
|
'HEAD',
|
|
'--',
|
|
])
|
|
})
|
|
|
|
test('on an unborn HEAD the child reads what is staged', async () => {
|
|
const git = gitOf()
|
|
const files = [rowOf('edited-since.ts'), rowOf('staged.ts')]
|
|
|
|
const read = await Git.fetchHunks(
|
|
git.run,
|
|
dataOf(files, {
|
|
isUnborn: true,
|
|
baseRef: '--cached',
|
|
stalePaths: ['edited-since.ts'],
|
|
}),
|
|
files,
|
|
)
|
|
|
|
expect(git.argvs[0]?.slice(-3)).toEqual(['--cached', '--', 'staged.ts'])
|
|
expect(read.get('edited-since.ts')).toEqual(Git.EMPTY_FILE_HUNKS)
|
|
expect(linesOf(read.get('staged.ts'))).toEqual([' one', '+in staged.ts'])
|
|
})
|
|
|
|
test('rows with no body are never named to git', async () => {
|
|
const git = gitOf()
|
|
|
|
const files = [
|
|
rowOf('app.ts'),
|
|
rowOf('logo.png', { isBinary: true }),
|
|
rowOf('moved.ts', { renamedFrom: 'old.ts' }),
|
|
rowOf('new.ts', { isUntracked: true }),
|
|
]
|
|
|
|
const read = await Git.fetchHunks(git.run, dataOf(files), files)
|
|
|
|
expect(git.asks).toEqual([['app.ts']])
|
|
|
|
expect(
|
|
['logo.png', 'moved.ts', 'new.ts'].map(path => read.get(path)),
|
|
).toEqual([
|
|
Git.EMPTY_FILE_HUNKS,
|
|
Git.EMPTY_FILE_HUNKS,
|
|
Git.EMPTY_FILE_HUNKS,
|
|
])
|
|
})
|
|
|
|
test('rows with no body alone start no child', async () => {
|
|
const git = gitOf()
|
|
const files = [rowOf('new.ts', { isUntracked: true })]
|
|
|
|
await Git.fetchHunks(git.run, dataOf(files), files)
|
|
|
|
expect(git.asks).toEqual([])
|
|
})
|
|
|
|
test('every file but the last gains the closing line', async () => {
|
|
const git = gitOf()
|
|
const files = [rowOf('a.ts'), rowOf('b.ts')]
|
|
const read = await Git.fetchHunks(git.run, dataOf(files), files)
|
|
|
|
expect(linesOf(read.get('a.ts'))).toEqual([' one', '+in a.ts', ' '])
|
|
expect(linesOf(read.get('b.ts'))).toEqual([' one', '+in b.ts'])
|
|
})
|
|
|
|
test('paths past the budget are asked of a second child', async () => {
|
|
const git = gitOf()
|
|
const read = await Git.fetchHunks(git.run, dataOf(LONG), LONG)
|
|
const paths = LONG.map(file => file.path)
|
|
|
|
expect(git.asks).toEqual([paths.slice(0, 2), paths.slice(2)])
|
|
|
|
expect(paths.map(path => linesOf(read.get(path))?.[1])).toEqual(
|
|
paths.map(path => `+in ${path}`),
|
|
)
|
|
})
|
|
|
|
test('one path past the budget is still asked, alone', async () => {
|
|
const git = gitOf()
|
|
const files = [rowOf('d'.repeat(Limits.MAX_PATHSPEC_CHARS + 1)), rowOf('e')]
|
|
|
|
await Git.fetchHunks(git.run, dataOf(files), files)
|
|
|
|
expect(git.asks.map(asked => asked.length)).toEqual([1, 1])
|
|
})
|
|
|
|
test('past the output cap the rest is asked again', async () => {
|
|
const files = [rowOf('a.ts'), rowOf('big.ts'), rowOf('c.ts'), rowOf('d.ts')]
|
|
|
|
const git = gitOf((asked, nth) => {
|
|
const isFirst = nth === 1
|
|
|
|
return isFirst
|
|
? Fixtures.ok(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', Fixtures.oneAddedOf('in a.ts')],
|
|
['big.ts', Fixtures.longLineOf(Limits.HOST_OUTPUT_CAP_BYTES)],
|
|
['c.ts', Fixtures.oneAddedOf('never arrived')],
|
|
['d.ts', Fixtures.oneAddedOf('never arrived')],
|
|
]).slice(0, Limits.HOST_OUTPUT_CAP_BYTES),
|
|
)
|
|
: Fixtures.hunksReadOf(asked)
|
|
})
|
|
|
|
const read = await Git.fetchHunks(git.run, dataOf(files), files)
|
|
|
|
expect(git.asks, 'the second list starts where the first stopped').toEqual([
|
|
['a.ts', 'big.ts', 'c.ts', 'd.ts'],
|
|
['c.ts', 'd.ts'],
|
|
])
|
|
|
|
expect(linesOf(read.get('a.ts'))).toEqual([' one', '+in a.ts', ' '])
|
|
expect(read.get('big.ts')).toEqual(Git.LARGE_FILE_HUNKS)
|
|
expect(linesOf(read.get('c.ts'))).toEqual([' one', '+in c.ts', ' '])
|
|
expect(linesOf(read.get('d.ts'))).toEqual([' one', '+in d.ts'])
|
|
})
|
|
|
|
test('a child that fails leaves every file unread', async () => {
|
|
const git = gitOf(() => Fixtures.GIT_TIMED_OUT)
|
|
const read = await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY)
|
|
|
|
expect(git.asks).toHaveLength(1)
|
|
expect([...read.values()].every(body => body === null)).toBe(true)
|
|
expect(read.size).toBe(Limits.MAX_FILES)
|
|
})
|
|
|
|
test('a second child that fails keeps what the first read', async () => {
|
|
const git = gitOf((asked, nth) => {
|
|
const isFirst = nth === 1
|
|
|
|
return isFirst ? Fixtures.hunksReadOf(asked) : Fixtures.UNSCRIPTED
|
|
})
|
|
|
|
const read = await Git.fetchHunks(git.run, dataOf(LONG), LONG)
|
|
const [first, second, third] = LONG.map(file => read.get(file.path))
|
|
|
|
expect(git.asks).toHaveLength(2)
|
|
expect(linesOf(first)?.[0]).toBe(' one')
|
|
expect(linesOf(second)?.[0]).toBe(' one')
|
|
expect(third).toBeNull()
|
|
})
|
|
|
|
test('a second child is paired by its own list', async () => {
|
|
const git = gitOf((asked, nth) => {
|
|
const isFirst = nth === 1
|
|
|
|
return Fixtures.hunksReadOf(isFirst ? asked : asked.toReversed())
|
|
})
|
|
|
|
const files = [...LONG, rowOf('z.ts')]
|
|
const read = await Git.fetchHunks(git.run, dataOf(files), files)
|
|
const [, , third] = LONG.map(file => file.path)
|
|
|
|
expect(git.asks[1]).toEqual([third, 'z.ts'])
|
|
expect(linesOf(read.get(third ?? ''))?.[1]).toBe(`+in ${third}`)
|
|
expect(linesOf(read.get('z.ts'))).toEqual([' one', '+in z.ts'])
|
|
})
|
|
|
|
test('an answer whose names and patches disagree ends the read', async () => {
|
|
const git = gitOf(() =>
|
|
Fixtures.ok(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', Fixtures.oneAddedOf('a'), 'diff --git a/o.ts b/o.ts'],
|
|
['b.ts', Fixtures.oneAddedOf('b')],
|
|
]),
|
|
),
|
|
)
|
|
|
|
const files = [rowOf('a.ts'), rowOf('b.ts')]
|
|
const read = await Git.fetchHunks(git.run, dataOf(files), files)
|
|
|
|
expect(git.asks, 'asked once, never again').toHaveLength(1)
|
|
|
|
expect([...read]).toEqual([
|
|
['a.ts', null],
|
|
['b.ts', null],
|
|
])
|
|
})
|
|
})
|