Files

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],
])
})
})