Files
orca/src/main/git/worktree-git-capabilities.test.ts
T
Kelvin Amoaba de77c4b065 fix(worktrees): list a folder once when git reports it twice (#24357)
When git lists the same folder twice (a leftover worktree registration that points at the main checkout), Orca's runtime listing turned each line into its own worktree with the same id, so `orca worktree current`, `active` and `branch:` failed with selector_ambiguous, and paired clients saw a duplicate row. The runtime scan now keeps git's first row per folder, the rule the desktop sidebar already uses. Separately, for a bare or separate-git-dir repo added through a linked worktree, the scan no longer relabels the main row with that worktree's folder (it relabels only when the folder's git dir is the common git dir), so the worktree keeps its own row and branch in the CLI and the sidebar. No extra git command runs.

Part of #23631: the "Profile state writer command timed out" toast in that issue has a separate cause.
2026-10-02 17:33:00 -07:00

139 lines
4.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const { gitExecFileAsyncMock } = vi.hoisted(() => ({
gitExecFileAsyncMock: vi.fn()
}))
vi.mock('./runner', () => ({
gitExecFileAsync: gitExecFileAsyncMock,
gitExecFileSync: vi.fn(),
translateWslOutputPaths: (output: string) => output
}))
import { clearGitCapabilityStateForTests } from './git-capability-state'
import { listWorktrees } from './worktree'
const WORKTREE_LIST_OUTPUT = `worktree /repo
HEAD abc123
branch refs/heads/main
`
describe('worktree Git capabilities', () => {
beforeEach(() => {
clearGitCapabilityStateForTests()
gitExecFileAsyncMock.mockReset()
})
it('does not repeat a known-unsupported -z probe on later scans', async () => {
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args.includes('-z')) {
return Promise.reject(
Object.assign(new Error('git usage error'), {
code: 129,
stderr: 'usage: git worktree list [<options>]\n'
})
)
}
return Promise.resolve({ stdout: WORKTREE_LIST_OUTPUT })
})
await listWorktrees('/repo')
await listWorktrees('/repo')
expect(gitExecFileAsyncMock.mock.calls.map(([args]) => args)).toEqual([
['worktree', 'list', '--porcelain', '-z'],
['worktree', 'list', '--porcelain'],
['worktree', 'list', '--porcelain']
])
})
it('keeps native and WSL Git capability results separate', async () => {
gitExecFileAsyncMock.mockImplementation(
(args: string[], options: { cwd: string; wslDistro?: string }) => {
if (args.includes('-z') && !options.wslDistro) {
return Promise.reject(
Object.assign(new Error('git usage error'), {
code: 129,
stderr: 'usage: git worktree list [<options>]\n'
})
)
}
return Promise.resolve({
stdout: `worktree ${options.cwd}\nHEAD abc123\nbranch refs/heads/main\n`
})
}
)
await listWorktrees('/native-repo')
await listWorktrees('/wsl-repo', { wslDistro: 'Ubuntu' })
expect(
gitExecFileAsyncMock.mock.calls.map(([args, options]) => ({
args,
wslDistro: options.wslDistro
}))
).toEqual([
{ args: ['worktree', 'list', '--porcelain', '-z'], wslDistro: undefined },
{ args: ['worktree', 'list', '--porcelain'], wslDistro: undefined },
{ args: ['worktree', 'list', '--porcelain', '-z'], wslDistro: 'Ubuntu' }
])
})
it('does not repeat a known-unsupported rev-parse --path-format probe', async () => {
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[0] === 'worktree') {
return Promise.resolve({
stdout: 'worktree /git-store/project.git\nHEAD abc123\nbranch refs/heads/main\n'
})
}
if (args.includes('--path-format=absolute')) {
return Promise.reject(
Object.assign(new Error('unknown option: --path-format=absolute'), {
stderr: 'error: unknown option `path-format=absolute`\n'
})
)
}
return Promise.resolve({ stdout: '/repo\n/git-store/project.git\n/git-store/project.git\n' })
})
await listWorktrees('/repo')
await listWorktrees('/repo')
const revParseCalls = gitExecFileAsyncMock.mock.calls.filter(
([args]) => (args as string[])[0] === 'rev-parse'
)
expect(revParseCalls.map(([args]) => args)).toEqual([
['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir', '--git-dir'],
['rev-parse', '--show-toplevel', '--git-common-dir', '--git-dir'],
['rev-parse', '--show-toplevel', '--git-common-dir', '--git-dir']
])
})
it('remembers old Git that echoes --path-format while exiting successfully', async () => {
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[0] === 'worktree') {
return Promise.resolve({
stdout: 'worktree /git-store/project.git\nHEAD abc123\nbranch refs/heads/main\n'
})
}
if (args.includes('--path-format=absolute')) {
return Promise.resolve({
stdout: '--path-format=absolute\n/repo\n/git-store/project.git\n/git-store/project.git\n'
})
}
return Promise.resolve({ stdout: '/repo\n/git-store/project.git\n/git-store/project.git\n' })
})
await listWorktrees('/repo')
await listWorktrees('/repo')
const revParseCalls = gitExecFileAsyncMock.mock.calls.filter(
([args]) => (args as string[])[0] === 'rev-parse'
)
expect(revParseCalls.map(([args]) => args)).toEqual([
['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir', '--git-dir'],
['rev-parse', '--show-toplevel', '--git-common-dir', '--git-dir']
])
})
})