Files
orca/src/relay/git-handler-worktree-paths.test.ts
T
Neil 8ba451e8dd fix: preserve newline worktree paths
Use porcelain-z worktree parsing with fallback line parsing so newline-containing worktree paths survive local and relay operations. Risk: low.
2026-05-31 01:50:16 -07:00

102 lines
3.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import type { GitExec } from './git-handler-ops'
import { removeWorktreeOp } from './git-handler-worktree-ops'
function lineWorktreeList(...entries: { path: string; branch?: string }[]): string {
return entries
.map((entry, index) =>
[
`worktree ${entry.path}`,
`HEAD ${index}`,
...(entry.branch ? [`branch refs/heads/${entry.branch}`] : [])
].join('\n')
)
.join('\n\n')
}
function nulWorktreeList(...entries: { path: string; branch?: string }[]): string {
return entries
.map((entry, index) =>
[
`worktree ${entry.path}`,
`HEAD ${index}`,
...(entry.branch ? [`branch refs/heads/${entry.branch}`] : []),
''
].join('\0')
)
.join('\0')
}
describe('relay worktree path parsing', () => {
it('deletes the matching branch for SSH worktrees whose paths contain newlines', async () => {
const worktreePath = '/repo-feature\nremote'
let listCount = 0
const git = vi.fn<GitExec>(async (args) => {
if (args[0] === 'rev-parse') {
return { stdout: '/repo/.git\n', stderr: '' }
}
if (args[0] === 'worktree' && args[1] === 'list') {
listCount += 1
return {
stdout:
listCount === 1
? nulWorktreeList(
{ path: '/repo', branch: 'main' },
{ path: worktreePath, branch: 'feature/newline' }
)
: nulWorktreeList({ path: '/repo', branch: 'main' }),
stderr: ''
}
}
return { stdout: '', stderr: '' }
})
await removeWorktreeOp(git, { worktreePath })
expect(git).toHaveBeenCalledWith(['branch', '-d', '--', 'feature/newline'], '/repo')
})
it('falls back to line-block worktree listing when remote Git rejects -z', async () => {
const calls: string[] = []
let listCount = 0
const git = vi.fn<GitExec>(async (args, cwd) => {
calls.push(`${cwd}$ ${args.join(' ')}`)
if (args[0] === 'rev-parse') {
return { stdout: '/repo/.git\n', stderr: '' }
}
if (args[0] === 'worktree' && args[1] === 'list' && args.includes('-z')) {
throw Object.assign(new Error("unknown switch `z'"), {
stderr: "error: unknown switch `z'"
})
}
if (args[0] === 'worktree' && args[1] === 'list') {
listCount += 1
return {
stdout:
listCount === 1
? lineWorktreeList(
{ path: '/repo', branch: 'main' },
{ path: '/repo-feature', branch: 'feature/test' }
)
: lineWorktreeList({ path: '/repo', branch: 'main' }),
stderr: ''
}
}
return { stdout: '', stderr: '' }
})
await removeWorktreeOp(git, { worktreePath: '/repo-feature' })
expect(calls).toEqual([
'/repo-feature$ rev-parse --git-common-dir',
'/repo$ worktree list --porcelain -z',
'/repo$ worktree list --porcelain',
'/repo$ worktree remove /repo-feature',
'/repo$ worktree prune',
'/repo$ worktree list --porcelain -z',
'/repo$ worktree list --porcelain',
'/repo$ branch -d -- feature/test'
])
})
})