/**
* test/http/diff.test.ts (N-diff-be, B1) — read-only git diff parsing + getDiff.
*
* Two layers:
* 1. Pure parsers (parseNumstat / parseUnifiedDiff) fed canned git output — the
* deterministic core (AC-B1.5: rename/new/delete/binary/empty/untracked, +/-
* consistent with `git diff --numstat`; never throws; `cmd` [31mred'
const patch = [
'diff --git a/x.txt b/x.txt',
'--- a/x.txt',
'+++ b/x.txt',
'@@ -0,0 +1 @@',
'+' + payload,
'',
].join('\n')
const [file] = parseUnifiedDiff(patch)
expect(file?.hunks[0]?.lines[0]).toEqual({ kind: 'added', text: payload })
})
it('parses several files in one patch', () => {
const patch = MODIFIED + [
'diff --git a/bar.txt b/bar.txt',
'--- a/bar.txt',
'+++ b/bar.txt',
'@@ -1 +1 @@',
'-one',
'+two',
'',
].join('\n')
const files = parseUnifiedDiff(patch)
expect(files.map((f) => f.newPath)).toEqual(['foo.txt', 'bar.txt'])
})
})
// ── getDiff (integration against a real git repo) ────────────────────────────
async function git(cwd: string, ...args: string[]): Promise {
await execFileAsync('git', args, { cwd })
}
describe('getDiff (real git repo)', () => {
let repo: string
beforeAll(async () => {
repo = await fs.mkdtemp(path.join(os.tmpdir(), 'webterm-diff-'))
await git(repo, 'init', '-q', '-b', 'main')
await git(repo, 'config', 'user.email', 'test@example.com')
await git(repo, 'config', 'user.name', 'Test')
await git(repo, 'config', 'commit.gpgsign', 'false')
await fs.writeFile(path.join(repo, 'tracked.txt'), 'one\ntwo\nthree\n')
await git(repo, 'add', '.')
await git(repo, 'commit', '-q', '-m', 'init')
})
afterAll(async () => {
await fs.rm(repo, { recursive: true, force: true })
})
it('returns structured working-tree diff with numstat-consistent counts', async () => {
await fs.writeFile(path.join(repo, 'tracked.txt'), 'one\nTWO\nthree\nfour\n')
const result = await getDiff(repo, { staged: false, cfg: LIMITS })
expect(result.staged).toBe(false)
expect(result.truncated).toBe(false)
const file = result.files.find((f) => f.newPath === 'tracked.txt')
expect(file).toBeDefined()
expect(file?.status).toBe('modified')
// changed line two + added line four = 2 added, 1 removed
expect(file?.added).toBe(2)
expect(file?.removed).toBe(1)
// revert for later tests
await fs.writeFile(path.join(repo, 'tracked.txt'), 'one\ntwo\nthree\n')
})
it('lists untracked files via porcelain (L3, working tree only)', async () => {
await fs.writeFile(path.join(repo, 'fresh.txt'), 'brand new\n')
const result = await getDiff(repo, { staged: false, cfg: LIMITS })
const untracked = result.files.find((f) => f.newPath === 'fresh.txt')
expect(untracked?.status).toBe('untracked')
await fs.rm(path.join(repo, 'fresh.txt'))
})
it('does not include untracked files in the staged diff', async () => {
await fs.writeFile(path.join(repo, 'staged-only.txt'), 'queued\n')
await git(repo, 'add', 'staged-only.txt')
const result = await getDiff(repo, { staged: true, cfg: LIMITS })
expect(result.staged).toBe(true)
const file = result.files.find((f) => f.newPath === 'staged-only.txt')
expect(file?.status).toBe('added')
expect(result.files.every((f) => f.status !== 'untracked')).toBe(true)
await git(repo, 'reset', '-q')
await fs.rm(path.join(repo, 'staged-only.txt'))
})
it('marks truncated when the file count exceeds diffMaxFiles', async () => {
for (let i = 0; i < 5; i++) {
await fs.writeFile(path.join(repo, `gen-${i}.txt`), `content ${i}\n`)
}
const result = await getDiff(repo, {
staged: false,
cfg: { ...LIMITS, diffMaxFiles: 2 },
})
expect(result.truncated).toBe(true)
expect(result.files.length).toBeLessThanOrEqual(2)
for (let i = 0; i < 5; i++) await fs.rm(path.join(repo, `gen-${i}.txt`))
})
it('never throws for a non-existent / non-git path (returns empty)', async () => {
const result = await getDiff(path.join(os.tmpdir(), 'definitely-not-here-xyz'), {
staged: false,
cfg: LIMITS,
})
expect(result.files).toEqual([])
expect(result.truncated).toBe(false)
})
})