mirror of
https://github.com/anthropics/claude-code.git
synced 2026-10-15 05:19:56 +08:00
316 lines
9.7 KiB
TypeScript
316 lines
9.7 KiB
TypeScript
import { describe, expect, test, tier } from 'claude-code/testing'
|
|
|
|
import Git from '../../../hooks/git'
|
|
import Parse from '../../../hooks/git/parse'
|
|
import Limits from '../../../hooks/limits'
|
|
import Fixtures from '../../fixtures'
|
|
|
|
tier('builtin')
|
|
|
|
describe('parse-file-diffs', () => {
|
|
const linesOf = (body: Git.FileHunks | undefined) =>
|
|
body?.hunks.flatMap(hunk => hunk.lines)
|
|
|
|
const cutAtCap = (printed: readonly Fixtures.PrintedFile[]) =>
|
|
Fixtures.hunksAnswerOf(printed).slice(0, Limits.HOST_OUTPUT_CAP_BYTES)
|
|
|
|
test('an empty answer: every file asked reads no hunks', () => {
|
|
expect([...Parse.parseFileDiffs('', ['a.ts', 'b.ts'])]).toEqual([
|
|
['a.ts', Git.EMPTY_FILE_HUNKS],
|
|
['b.ts', Git.EMPTY_FILE_HUNKS],
|
|
])
|
|
})
|
|
|
|
test('each unusual name holds its own patch', () => {
|
|
expect(
|
|
[
|
|
...Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf(Fixtures.UNUSUAL_NAMES),
|
|
Fixtures.UNUSUAL_NAMES.map(([path]) => path),
|
|
),
|
|
].map(([path, body]) => [path, linesOf(body)]),
|
|
).toEqual(
|
|
Fixtures.UNUSUAL_NAMES.map(([path, body]) => [
|
|
path,
|
|
body.split('\n').slice(1, -1),
|
|
]),
|
|
)
|
|
})
|
|
|
|
test('the order asked in does not move a patch', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', Fixtures.oneAddedOf('first')],
|
|
['b.ts', Fixtures.oneAddedOf('next')],
|
|
]),
|
|
['b.ts', 'a.ts'],
|
|
)
|
|
|
|
expect(linesOf(read.get('a.ts'))).toEqual([' one', '+first'])
|
|
expect(linesOf(read.get('b.ts'))).toEqual([' one', '+next'])
|
|
})
|
|
|
|
test('content that looks like an opening line stays content', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
[
|
|
'a.ts',
|
|
'@@ -1,2 +1,2 @@\n diff --git a/b.ts b/b.ts\n' +
|
|
'-diff --git a/x b/x\n+diff --git a/y b/y\n',
|
|
],
|
|
['b.ts', Fixtures.oneAddedOf('its own')],
|
|
]),
|
|
['a.ts', 'b.ts'],
|
|
)
|
|
|
|
expect(linesOf(read.get('a.ts'))).toEqual([
|
|
' diff --git a/b.ts b/b.ts',
|
|
'-diff --git a/x b/x',
|
|
'+diff --git a/y b/y',
|
|
])
|
|
|
|
expect(linesOf(read.get('b.ts'))).toEqual([' one', '+its own'])
|
|
})
|
|
|
|
test('a file turned link: its two patches are one file', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', Fixtures.oneAddedOf('before')],
|
|
[
|
|
'link',
|
|
'deleted file mode 100644\n@@ -1 +0,0 @@\n-one\n' +
|
|
'diff --git a/link b/link\nnew file mode 120000\n' +
|
|
'@@ -0,0 +1 @@\n+a.ts\n',
|
|
],
|
|
['z.ts', Fixtures.oneAddedOf('after')],
|
|
]),
|
|
['a.ts', 'link', 'z.ts'],
|
|
)
|
|
|
|
expect(linesOf(read.get('a.ts'))).toEqual([' one', '+before'])
|
|
expect(linesOf(read.get('link'))).toEqual(['-one', '+a.ts'])
|
|
expect(linesOf(read.get('z.ts'))).toEqual([' one', '+after'])
|
|
})
|
|
|
|
test('a conflicted file read against HEAD is one more file', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', Fixtures.oneAddedOf('before')],
|
|
[
|
|
'both changed.ts',
|
|
'@@ -1 +1,5 @@\n+<<<<<<< ours\n one\n+=======\n+theirs\n+>>>>>>> ' +
|
|
'theirs\n',
|
|
],
|
|
['gone on our side.ts', '@@ -0,0 +1 @@\n+theirs\n'],
|
|
['z.ts', Fixtures.oneAddedOf('after')],
|
|
]).replace(
|
|
Fixtures.rawRecordOf('gone on our side.ts'),
|
|
Fixtures.rawRecordOf('gone on our side.ts').replace(' M\0', ' A\0'),
|
|
),
|
|
['a.ts', 'both changed.ts', 'gone on our side.ts', 'z.ts'],
|
|
)
|
|
|
|
expect(linesOf(read.get('a.ts'))).toEqual([' one', '+before'])
|
|
|
|
expect(linesOf(read.get('both changed.ts'))).toEqual([
|
|
'+<<<<<<< ours',
|
|
' one',
|
|
'+=======',
|
|
'+theirs',
|
|
'+>>>>>>> theirs',
|
|
])
|
|
|
|
expect(linesOf(read.get('gone on our side.ts'))).toEqual(['+theirs'])
|
|
expect(linesOf(read.get('z.ts'))).toEqual([' one', '+after'])
|
|
})
|
|
|
|
test('an unmerged path has no patch and reads no hunks', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', `${Fixtures.oneAddedOf('before')}* Unmerged path c.ts\n`],
|
|
['z.ts', Fixtures.oneAddedOf('after')],
|
|
]).replace(
|
|
Fixtures.rawRecordOf('z.ts'),
|
|
Fixtures.rawRecordOf('c.ts').replace(' M\0', ' U\0') +
|
|
Fixtures.rawRecordOf('z.ts'),
|
|
),
|
|
['a.ts', 'c.ts', 'z.ts'],
|
|
)
|
|
|
|
expect(linesOf(read.get('a.ts'))).toEqual([' one', '+before'])
|
|
expect(read.get('c.ts')).toEqual(Git.EMPTY_FILE_HUNKS)
|
|
expect(linesOf(read.get('z.ts'))).toEqual([' one', '+after'])
|
|
})
|
|
|
|
test('a file git no longer lists reads no hunks', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([['kept.ts', Fixtures.oneAddedOf('changed')]]),
|
|
['gone.ts', 'kept.ts'],
|
|
)
|
|
|
|
expect(read.get('gone.ts')).toEqual(Git.EMPTY_FILE_HUNKS)
|
|
expect(linesOf(read.get('kept.ts'))).toEqual([' one', '+changed'])
|
|
})
|
|
|
|
test('a listed file nobody asked for is left out', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['made', '@@ -1 +0,0 @@\n-a file\n'],
|
|
['made/inside.ts', '@@ -0,0 +1 @@\n+now a folder\n'],
|
|
]),
|
|
['made'],
|
|
)
|
|
|
|
expect([...read.keys()]).toEqual(['made'])
|
|
expect(linesOf(read.get('made'))).toEqual(['-a file'])
|
|
})
|
|
|
|
test('with no empty field before the patches it reads the same', () => {
|
|
expect(
|
|
linesOf(
|
|
Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', Fixtures.oneAddedOf('unparted')],
|
|
]).replace('\0\0', '\0'),
|
|
['a.ts'],
|
|
).get('a.ts'),
|
|
),
|
|
).toEqual([' one', '+unparted'])
|
|
})
|
|
|
|
test('fewer patches than names settle nothing', () => {
|
|
expect(
|
|
Parse.parseFileDiffs(
|
|
Fixtures.rawRecordOf('a.ts') +
|
|
Fixtures.hunksAnswerOf([['b.ts', Fixtures.oneAddedOf('whose')]]),
|
|
['a.ts', 'b.ts'],
|
|
).size,
|
|
).toBe(0)
|
|
})
|
|
|
|
test('a patch opened under another name settles nothing', () => {
|
|
expect(
|
|
Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['a.ts', Fixtures.oneAddedOf('here'), 'diff --git a/b.ts b/b.ts'],
|
|
['b.ts', Fixtures.oneAddedOf('there'), 'diff --git a/a.ts b/a.ts'],
|
|
]),
|
|
['a.ts', 'b.ts'],
|
|
).size,
|
|
).toBe(0)
|
|
})
|
|
|
|
test('patches git coloured: no hunks, as one file read alone', () => {
|
|
expect([
|
|
...Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
[
|
|
'a.ts',
|
|
'\u001b[36m@@ -1 +1,2 @@\u001b[m\n one\n\u001b[32m+two\u001b[m\n',
|
|
'\u001b[1mdiff --git a/a.ts b/a.ts\u001b[m',
|
|
],
|
|
]),
|
|
['a.ts'],
|
|
),
|
|
]).toEqual([['a.ts', Git.EMPTY_FILE_HUNKS]])
|
|
})
|
|
|
|
test('the line cap and the size cap hold file by file', () => {
|
|
const long = Array.from(
|
|
{ length: Limits.MAX_LINES_PER_FILE + Fixtures.PAST_THE_CAP },
|
|
(_, at) => `+${at}`,
|
|
).join('\n')
|
|
|
|
const read = Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['large.ts', Fixtures.longLineOf(Limits.MAX_DIFF_BYTES)],
|
|
['long.ts', `@@ -0,0 +1,405 @@\n${long}\n`],
|
|
['small.ts', Fixtures.oneAddedOf('whole')],
|
|
]),
|
|
['large.ts', 'long.ts', 'small.ts'],
|
|
)
|
|
|
|
expect(read.get('large.ts')).toEqual(Git.LARGE_FILE_HUNKS)
|
|
expect(read.get('long.ts')?.isTruncated).toBe(true)
|
|
|
|
expect(linesOf(read.get('long.ts'))).toHaveLength(Limits.MAX_LINES_PER_FILE)
|
|
|
|
expect(read.get('small.ts')?.isTruncated).toBe(false)
|
|
expect(linesOf(read.get('small.ts'))).toEqual([' one', '+whole'])
|
|
})
|
|
|
|
test('a file at the size cap to the character is not large', () => {
|
|
const room =
|
|
Limits.MAX_DIFF_BYTES -
|
|
Fixtures.hunksAnswerOf([['edge.ts', Fixtures.longLineOf(0)]]).replace(
|
|
/^.*\0/s,
|
|
'',
|
|
).length
|
|
|
|
const largePast = (extra: number) =>
|
|
Parse.parseFileDiffs(
|
|
Fixtures.hunksAnswerOf([
|
|
['edge.ts', Fixtures.longLineOf(room + extra)],
|
|
['next.ts', Fixtures.oneAddedOf('after it')],
|
|
]),
|
|
['edge.ts', 'next.ts'],
|
|
).get('edge.ts')?.isLarge
|
|
|
|
expect(largePast(0)).toBe(false)
|
|
expect(largePast(1)).toBe(true)
|
|
})
|
|
|
|
test('cut by the host: whole files settle, the rest wait', () => {
|
|
const read = Parse.parseFileDiffs(
|
|
cutAtCap([
|
|
['a.ts', Fixtures.oneAddedOf('whole')],
|
|
['big.ts', Fixtures.longLineOf(Limits.MAX_DIFF_BYTES)],
|
|
['cut.ts', Fixtures.longLineOf(Limits.HOST_OUTPUT_CAP_BYTES)],
|
|
['unseen.ts', Fixtures.oneAddedOf('never arrived')],
|
|
]),
|
|
['a.ts', 'big.ts', 'cut.ts', 'unseen.ts'],
|
|
)
|
|
|
|
expect([...read.keys()]).toEqual(['a.ts', 'big.ts', 'cut.ts'])
|
|
expect(linesOf(read.get('a.ts'))).toEqual([' one', '+whole'])
|
|
expect(read.get('big.ts')).toEqual(Git.LARGE_FILE_HUNKS)
|
|
|
|
expect(read.get('cut.ts'), 'what arrived of it is already large').toEqual(
|
|
Git.LARGE_FILE_HUNKS,
|
|
)
|
|
})
|
|
|
|
test('cut by the host inside a small file: that file waits', () => {
|
|
expect([
|
|
...Parse.parseFileDiffs(
|
|
cutAtCap([
|
|
[
|
|
'big.ts',
|
|
Fixtures.longLineOf(
|
|
Limits.HOST_OUTPUT_CAP_BYTES - Limits.MAX_DIFF_BYTES / 2,
|
|
),
|
|
],
|
|
['cut.ts', Fixtures.longLineOf(Limits.MAX_DIFF_BYTES)],
|
|
['unseen.ts', Fixtures.oneAddedOf('never arrived')],
|
|
]),
|
|
['big.ts', 'cut.ts', 'unseen.ts'],
|
|
),
|
|
]).toEqual([['big.ts', Git.LARGE_FILE_HUNKS]])
|
|
})
|
|
|
|
test('cut by the host: a file git did not list reads no hunks', () => {
|
|
expect([
|
|
...Parse.parseFileDiffs(
|
|
cutAtCap([
|
|
['cut.ts', Fixtures.longLineOf(Limits.HOST_OUTPUT_CAP_BYTES)],
|
|
['unseen.ts', Fixtures.oneAddedOf('never arrived')],
|
|
]),
|
|
['cut.ts', 'gone.ts', 'unseen.ts'],
|
|
),
|
|
]).toEqual([
|
|
['cut.ts', Git.LARGE_FILE_HUNKS],
|
|
['gone.ts', Git.EMPTY_FILE_HUNKS],
|
|
])
|
|
})
|
|
})
|